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,147 @@
|
|
|
1
|
+
"""Typing stub for the vendored VRMCS transport.
|
|
2
|
+
|
|
3
|
+
``vrmcs_client.py`` is vendored verbatim from VRMCS-py-client and a handful of
|
|
4
|
+
its methods carry no return annotation. While it arrived inside the
|
|
5
|
+
``vention-firmware-grpc-client`` wheel -- which ships no ``py.typed`` -- mypy
|
|
6
|
+
treated the whole client as ``Any`` and never looked inside. griip-core *does*
|
|
7
|
+
ship ``py.typed``, so without a stub mypy starts analysing upstream code we do
|
|
8
|
+
not maintain and reports every call into the unannotated methods as untyped.
|
|
9
|
+
|
|
10
|
+
Generated with ``stubgen`` from the vendored module and kept beside it, so
|
|
11
|
+
callers get the real request/response types rather than ``Any``. Regenerate it
|
|
12
|
+
whenever the vendored copy is refreshed:
|
|
13
|
+
|
|
14
|
+
uv run stubgen -o <tmp> src/griip_core/hardware/vrmcs_client.py
|
|
15
|
+
|
|
16
|
+
Annotating the client itself belongs upstream in VRMCS-py-client -- edits to
|
|
17
|
+
the vendored copy are lost on the next refresh, which is why the types live
|
|
18
|
+
here instead.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import threading
|
|
22
|
+
from _typeshed import Incomplete
|
|
23
|
+
from typing import Callable
|
|
24
|
+
from vention.robots.v1.move_interpolation_request_pb2 import (
|
|
25
|
+
MoveInterpolationRequest as MoveInterpolationRequest,
|
|
26
|
+
MoveInterpolationResponse as MoveInterpolationResponse,
|
|
27
|
+
)
|
|
28
|
+
from vention.robots.v1.move_request_pb2 import (
|
|
29
|
+
MoveRequest as MoveRequest,
|
|
30
|
+
MoveResponse as MoveResponse,
|
|
31
|
+
WaitForMoveCompletionRequest as WaitForMoveCompletionRequest,
|
|
32
|
+
WaitForMoveCompletionResponse as WaitForMoveCompletionResponse,
|
|
33
|
+
)
|
|
34
|
+
from vention.robots.v1.move_until_pb2 import MoveUntilRequest as MoveUntilRequest, MoveUntilResponse as MoveUntilResponse
|
|
35
|
+
from vention.robots.v1.plan_request_pb2 import PlanRequest as PlanRequest, PlanResponse as PlanResponse
|
|
36
|
+
from vention.robots.v1.robot_alarm_pb2 import RobotAlarm as RobotAlarm, RobotAlarmStreamRequest as RobotAlarmStreamRequest
|
|
37
|
+
from vention.robots.v1.robot_configuration_pb2 import (
|
|
38
|
+
GetTcpRequest as GetTcpRequest,
|
|
39
|
+
GetTcpResponse as GetTcpResponse,
|
|
40
|
+
SetPayloadRequest as SetPayloadRequest,
|
|
41
|
+
SetPayloadResponse as SetPayloadResponse,
|
|
42
|
+
SetTcpRequest as SetTcpRequest,
|
|
43
|
+
SetTcpResponse as SetTcpResponse,
|
|
44
|
+
)
|
|
45
|
+
from vention.robots.v1.robot_feedback_pb2 import (
|
|
46
|
+
Feedback as Feedback,
|
|
47
|
+
FeedbackStreamRequest as FeedbackStreamRequest,
|
|
48
|
+
GetFeedbackRequest as GetFeedbackRequest,
|
|
49
|
+
GetFeedbackResponse as GetFeedbackResponse,
|
|
50
|
+
)
|
|
51
|
+
from vention.robots.v1.robot_kinematics_pb2 import (
|
|
52
|
+
ForwardKinematicsRequest as ForwardKinematicsRequest,
|
|
53
|
+
ForwardKinematicsResponse as ForwardKinematicsResponse,
|
|
54
|
+
InverseKinematicsBatchRequest as InverseKinematicsBatchRequest,
|
|
55
|
+
InverseKinematicsBatchResponse as InverseKinematicsBatchResponse,
|
|
56
|
+
InverseKinematicsRequest as InverseKinematicsRequest,
|
|
57
|
+
InverseKinematicsResponse as InverseKinematicsResponse,
|
|
58
|
+
)
|
|
59
|
+
from vention.robots.v1.robot_management_pb2 import (
|
|
60
|
+
ListRobotsResponse as ListRobotsResponse,
|
|
61
|
+
RegisterRobotRequest as RegisterRobotRequest,
|
|
62
|
+
RegisterRobotResponse,
|
|
63
|
+
RobotInfo as RobotInfo,
|
|
64
|
+
UnregisterRobotResponse,
|
|
65
|
+
)
|
|
66
|
+
from vention.robots.v1.robot_requests_pb2 import (
|
|
67
|
+
GetRobotInfoRequest as GetRobotInfoRequest,
|
|
68
|
+
JointStatesStreamRequest as JointStatesStreamRequest,
|
|
69
|
+
PositionStreamRequest as PositionStreamRequest,
|
|
70
|
+
StopRequest as StopRequest,
|
|
71
|
+
UnregisterRobotRequest as UnregisterRobotRequest,
|
|
72
|
+
)
|
|
73
|
+
from vention.robots.v1.robot_state_pb2 import (
|
|
74
|
+
CommandState as CommandState,
|
|
75
|
+
CommandStateStreamRequest as CommandStateStreamRequest,
|
|
76
|
+
GetCommandStateRequest as GetCommandStateRequest,
|
|
77
|
+
GetCommandStateResponse as GetCommandStateResponse,
|
|
78
|
+
GetJointStatesRequest as GetJointStatesRequest,
|
|
79
|
+
GetJointStatesResponse as GetJointStatesResponse,
|
|
80
|
+
GetPositionStateRequest as GetPositionStateRequest,
|
|
81
|
+
GetPositionStateResponse as GetPositionStateResponse,
|
|
82
|
+
GetRobotStatesRequest as GetRobotStatesRequest,
|
|
83
|
+
GetRobotStatesResponse as GetRobotStatesResponse,
|
|
84
|
+
JointStates as JointStates,
|
|
85
|
+
PositionState as PositionState,
|
|
86
|
+
RobotStates as RobotStates,
|
|
87
|
+
RobotStatesStreamRequest as RobotStatesStreamRequest,
|
|
88
|
+
SetToFreedriveRequest as SetToFreedriveRequest,
|
|
89
|
+
SetToFreedriveResponse as SetToFreedriveResponse,
|
|
90
|
+
SetToNormalRequest as SetToNormalRequest,
|
|
91
|
+
SetToNormalResponse as SetToNormalResponse,
|
|
92
|
+
)
|
|
93
|
+
from vention.robots.v1.stop_request_pb2 import StopResponse as StopResponse
|
|
94
|
+
|
|
95
|
+
logger: Incomplete
|
|
96
|
+
|
|
97
|
+
class VRMCSClient:
|
|
98
|
+
channel: Incomplete
|
|
99
|
+
stub: Incomplete
|
|
100
|
+
joint_stream_thread: threading.Thread | None
|
|
101
|
+
position_stream_thread: threading.Thread | None
|
|
102
|
+
states_stream_thread: threading.Thread | None
|
|
103
|
+
command_state_stream_thread: threading.Thread | None
|
|
104
|
+
feedback_stream_thread: threading.Thread | None
|
|
105
|
+
robot_alarm_stream_thread: threading.Thread | None
|
|
106
|
+
def __init__(self, channel_url: str) -> None: ...
|
|
107
|
+
def register_robot(self, register_robot_req: RegisterRobotRequest) -> RegisterRobotResponse: ...
|
|
108
|
+
def stop(self, stop_req: StopRequest) -> StopResponse: ...
|
|
109
|
+
def set_to_normal(self, set_to_normal_req: SetToNormalRequest) -> SetToNormalResponse: ...
|
|
110
|
+
def set_to_freedrive(self, set_to_freedrive_req: SetToFreedriveRequest) -> SetToFreedriveResponse: ...
|
|
111
|
+
def move(self, move_req: MoveRequest) -> MoveResponse: ...
|
|
112
|
+
def plan(self, plan_req: PlanRequest) -> PlanResponse: ...
|
|
113
|
+
def wait_for_move_completion(self, request: WaitForMoveCompletionRequest) -> WaitForMoveCompletionResponse: ...
|
|
114
|
+
def move_until(self, move_until_req: MoveUntilRequest) -> MoveUntilResponse: ...
|
|
115
|
+
def joint_stream_subscribe(self, stream_req: JointStatesStreamRequest, on_data: Callable[[JointStates], None]): ...
|
|
116
|
+
joint_stream_iterator: Incomplete
|
|
117
|
+
def joint_stream_unsubscribe(self) -> None: ...
|
|
118
|
+
def position_stream_subscribe(self, stream_req: PositionStreamRequest, on_data: Callable[[PositionState], None]): ...
|
|
119
|
+
position_stream_iterator: Incomplete
|
|
120
|
+
def position_stream_unsubscribe(self) -> None: ...
|
|
121
|
+
def states_stream_subscribe(self, stream_req: RobotStatesStreamRequest, on_data: Callable[[RobotStates], None]): ...
|
|
122
|
+
states_stream_iterator: Incomplete
|
|
123
|
+
def states_stream_unsubscribe(self) -> None: ...
|
|
124
|
+
def move_interpolation(self, move_interp_req: MoveInterpolationRequest) -> MoveInterpolationResponse: ...
|
|
125
|
+
def list_robots(self) -> ListRobotsResponse: ...
|
|
126
|
+
def set_payload(self, set_payload_req: SetPayloadRequest) -> SetPayloadResponse: ...
|
|
127
|
+
def unregister_robot(self, unregister_robot_req: UnregisterRobotRequest) -> UnregisterRobotResponse: ...
|
|
128
|
+
def get_robot_info(self, get_robot_info_req: GetRobotInfoRequest) -> RobotInfo: ...
|
|
129
|
+
def command_state_stream_subscribe(self, stream_req: CommandStateStreamRequest, on_data: Callable[[CommandState], None]): ...
|
|
130
|
+
command_state_stream_iterator: Incomplete
|
|
131
|
+
def command_state_stream_unsubscribe(self) -> None: ...
|
|
132
|
+
def inverse_kinematics_batch(self, ik_req: InverseKinematicsBatchRequest) -> InverseKinematicsBatchResponse: ...
|
|
133
|
+
def inverse_kinematics(self, ik_req: InverseKinematicsRequest) -> InverseKinematicsResponse: ...
|
|
134
|
+
def forward_kinematics(self, fk_req: ForwardKinematicsRequest) -> ForwardKinematicsResponse: ...
|
|
135
|
+
def set_tcp(self, set_tcp_req: SetTcpRequest) -> SetTcpResponse: ...
|
|
136
|
+
def get_tcp(self, get_tcp_req: GetTcpRequest) -> GetTcpResponse: ...
|
|
137
|
+
def get_joint_states(self, get_joint_states_req: GetJointStatesRequest) -> GetJointStatesResponse: ...
|
|
138
|
+
def get_position_state(self, get_position_state_req: GetPositionStateRequest) -> GetPositionStateResponse: ...
|
|
139
|
+
def get_robot_states(self, get_robot_states_req: GetRobotStatesRequest) -> GetRobotStatesResponse: ...
|
|
140
|
+
def get_command_state(self, get_command_state_req: GetCommandStateRequest) -> GetCommandStateResponse: ...
|
|
141
|
+
def get_feedback(self, get_feedback_req: GetFeedbackRequest) -> GetFeedbackResponse: ...
|
|
142
|
+
def feedback_stream_subscribe(self, stream_req: FeedbackStreamRequest, on_data: Callable[[Feedback], None]): ...
|
|
143
|
+
feedback_stream_iterator: Incomplete
|
|
144
|
+
def feedback_stream_unsubscribe(self) -> None: ...
|
|
145
|
+
def robot_alarm_stream_subscribe(self, stream_req: RobotAlarmStreamRequest, on_data: Callable[[RobotAlarm], None]): ...
|
|
146
|
+
robot_alarm_stream_iterator: Incomplete
|
|
147
|
+
def robot_alarm_stream_unsubscribe(self) -> None: ...
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Shared helper for adopting existing VRMCS robot registrations.
|
|
2
|
+
|
|
3
|
+
VRMCS self-registers robots at id='0' on startup and holds the UR's RTDE
|
|
4
|
+
registers. Re-registering the same id with different driver params is
|
|
5
|
+
rejected by the server, and tearing down to re-register causes RTDE
|
|
6
|
+
contention. Adopting the existing registration sidesteps that fight.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from google.protobuf import empty_pb2
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_compatible_registration(
|
|
18
|
+
vrmcs_client: Any,
|
|
19
|
+
robot_id: str,
|
|
20
|
+
robot_ip: str,
|
|
21
|
+
quiet: bool = False,
|
|
22
|
+
require_ip_match: bool = True,
|
|
23
|
+
allow_legacy_zero_id_match: bool = False,
|
|
24
|
+
) -> Optional[Any]:
|
|
25
|
+
"""Return a registered VRMCS RobotInfo we should adopt, or None.
|
|
26
|
+
|
|
27
|
+
Resolution order:
|
|
28
|
+
1. Same id as ``robot_id``. When ``require_ip_match=True`` (default),
|
|
29
|
+
the registration's IP must also match ``robot_ip`` (or be empty,
|
|
30
|
+
treated as wildcard for legacy registrations that don't populate
|
|
31
|
+
driver_params.ip).
|
|
32
|
+
2. Different id, IP matches, registered id is non-"0". This is the
|
|
33
|
+
execution-engine UUID case — VRMCS' EE registers under a UUID
|
|
34
|
+
while older clients still pass id="0".
|
|
35
|
+
3. Different id, IP matches, registered id is "0". Legacy single-
|
|
36
|
+
robot fallback. Opt-in via ``allow_legacy_zero_id_match`` because
|
|
37
|
+
a stray id="0" with the same IP is ambiguous in multi-robot cells.
|
|
38
|
+
|
|
39
|
+
A registration that matches neither the id nor the IP is never
|
|
40
|
+
adopted — silently attaching to a different robot on the network is
|
|
41
|
+
unsafe. Callers should fall back to fresh registration when this
|
|
42
|
+
returns None.
|
|
43
|
+
|
|
44
|
+
If ListRobots fails (e.g. pydantic-conversion issues in certain
|
|
45
|
+
deployed VRMCS client builds), logs a warning and returns None so
|
|
46
|
+
the caller can fall back to fresh registration.
|
|
47
|
+
|
|
48
|
+
Set ``quiet=True`` to suppress the library's warnings; useful when
|
|
49
|
+
the caller polls and handles its own dedup.
|
|
50
|
+
|
|
51
|
+
Set ``require_ip_match=False`` to opt out of the step-1 IP guard
|
|
52
|
+
when the caller knows the id is authoritative regardless of IP
|
|
53
|
+
(e.g. tooling that targets a specific registration by id).
|
|
54
|
+
|
|
55
|
+
Set ``allow_legacy_zero_id_match=True`` to enable step 3. Pass this
|
|
56
|
+
only when the caller knows the network is single-robot.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
registered = list(vrmcs_client.stub.ListRobots(empty_pb2.Empty()).robots)
|
|
60
|
+
except Exception as list_error:
|
|
61
|
+
if not quiet:
|
|
62
|
+
logger.warning("ListRobots failed, cannot adopt existing registration: %s", list_error)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def ip_of(info: Any) -> str:
|
|
66
|
+
return str(info.driver_params.params.get("ip", ""))
|
|
67
|
+
|
|
68
|
+
# Step 1: same id as caller. IP must match (or be wildcard) unless caller opts out.
|
|
69
|
+
for info in registered:
|
|
70
|
+
if info.id != robot_id:
|
|
71
|
+
continue
|
|
72
|
+
if not require_ip_match or ip_of(info) in ("", robot_ip):
|
|
73
|
+
return info
|
|
74
|
+
# Same id, wrong IP — defer to ip-based resolution.
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
# Step 2: different id, IP matches, non-"0" id (execution-engine UUID).
|
|
78
|
+
for info in registered:
|
|
79
|
+
if ip_of(info) == robot_ip and info.id != "0":
|
|
80
|
+
return info
|
|
81
|
+
|
|
82
|
+
# Step 3: different id, IP matches, id="0" (legacy single-robot fallback).
|
|
83
|
+
if allow_legacy_zero_id_match:
|
|
84
|
+
for info in registered:
|
|
85
|
+
if ip_of(info) == robot_ip and info.id == "0":
|
|
86
|
+
return info
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def adopt_existing_registration(
|
|
92
|
+
vrmcs_client: Any,
|
|
93
|
+
robot_instance: Any,
|
|
94
|
+
*,
|
|
95
|
+
log: Callable[[str], None] = logger.info,
|
|
96
|
+
allow_legacy_zero_id_match: bool = False,
|
|
97
|
+
prefer_ip_match: bool = False,
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""Look up an adoptable VRMCS registration for ``robot_instance`` and adopt it.
|
|
100
|
+
|
|
101
|
+
Mutates ``robot_instance.id`` in place when the adopted registration's id
|
|
102
|
+
differs from the caller's id (the execution-engine UUID case). Returns
|
|
103
|
+
``True`` when a registration was adopted so the caller can skip the fresh
|
|
104
|
+
registration path; ``False`` when no compatible registration exists.
|
|
105
|
+
|
|
106
|
+
``log`` is invoked with the adoption message; defaults to module logger
|
|
107
|
+
info. Pass a wrapper for callers with custom logging interfaces.
|
|
108
|
+
|
|
109
|
+
Set ``prefer_ip_match=True`` when the caller has no explicit robot id. This
|
|
110
|
+
skips exact id matching for placeholder id "0", allowing the non-zero
|
|
111
|
+
execution-engine registration with the same IP to be adopted. If no such
|
|
112
|
+
registration exists, callers can still fall back to registering id "0".
|
|
113
|
+
"""
|
|
114
|
+
robot_ip = robot_instance.driver_params.params.get("ip", "")
|
|
115
|
+
query_id = "__auto_by_ip__" if prefer_ip_match else robot_instance.id
|
|
116
|
+
existing = find_compatible_registration(
|
|
117
|
+
vrmcs_client,
|
|
118
|
+
robot_id=query_id,
|
|
119
|
+
robot_ip=robot_ip,
|
|
120
|
+
allow_legacy_zero_id_match=allow_legacy_zero_id_match,
|
|
121
|
+
)
|
|
122
|
+
if existing is None:
|
|
123
|
+
return False
|
|
124
|
+
if existing.id != robot_instance.id:
|
|
125
|
+
log(f"Adopting existing VRMCS registration id={existing.id!r} " f"(was {robot_instance.id!r}) for robot at {robot_ip}")
|
|
126
|
+
robot_instance.id = existing.id
|
|
127
|
+
else:
|
|
128
|
+
log("Robot already registered with VRMCS, proceeding.")
|
|
129
|
+
return True
|
griip_core/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Robot driver names shared by the SDK cell config and the server's InitializeRequest."""
|
|
2
|
+
|
|
3
|
+
# The app owns the robot (e.g. a Fanuc over RMI). Nothing registers it in VRMCS,
|
|
4
|
+
# the server skips IK and path planning, and the SDK reads pose, joints and IK
|
|
5
|
+
# from a robot object the app passes in.
|
|
6
|
+
EXTERNAL_ROBOT_DRIVER = "external"
|
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Joint-angle math helpers used internally by the SDK.
|
|
2
|
+
|
|
3
|
+
Kept here (rather than in ``mmai_griip_api.utils.joint_math``) so the
|
|
4
|
+
published griip-sdk wheel can be imported without the API app installed.
|
|
5
|
+
``mmai_griip_api.utils.joint_math`` re-exports these for backward compat.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional, TypeVar, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
JointConfiguration = list[float]
|
|
18
|
+
JointConfigurations = Union[list[float], list[list[float]]]
|
|
19
|
+
|
|
20
|
+
_JointConfigurationsT = TypeVar("_JointConfigurationsT", list[float], list[list[float]])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalize_joint_configurations_to_reference(
|
|
24
|
+
joint_configurations: _JointConfigurationsT,
|
|
25
|
+
reference_joint_angles: list[float],
|
|
26
|
+
joint_lower_limits: Optional[list[float]] = None,
|
|
27
|
+
joint_upper_limits: Optional[list[float]] = None,
|
|
28
|
+
) -> _JointConfigurationsT:
|
|
29
|
+
"""Wrap each joint to the equivalent angle closest to the reference configuration.
|
|
30
|
+
|
|
31
|
+
Accepts a single configuration (1-D) or a batch (2-D); returns the same shape.
|
|
32
|
+
|
|
33
|
+
Pass the joint limits whenever they are known. The closest equivalent is the
|
|
34
|
+
shortest move but not always a legal one: from a reference already part-way
|
|
35
|
+
round, it can land a further revolution out and drive the joint into its hard
|
|
36
|
+
stop. With limits supplied, such a joint falls back to the opposite
|
|
37
|
+
revolution instead, which is the longer move but a reachable one.
|
|
38
|
+
"""
|
|
39
|
+
reference = np.asarray(reference_joint_angles, dtype=float)
|
|
40
|
+
joint_positions = np.asarray(joint_configurations, dtype=float)
|
|
41
|
+
|
|
42
|
+
if joint_positions.ndim == 1:
|
|
43
|
+
is_compatible = joint_positions.shape == reference.shape
|
|
44
|
+
elif joint_positions.ndim == 2:
|
|
45
|
+
is_compatible = joint_positions.shape[1] == reference.shape[0]
|
|
46
|
+
else:
|
|
47
|
+
is_compatible = False
|
|
48
|
+
|
|
49
|
+
if not is_compatible:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"joint_configurations has shape {joint_positions.shape}, " f"but reference_joint_angles has length {reference.shape[0]}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
two_pi = 2.0 * np.pi
|
|
55
|
+
delta = (joint_positions - reference + np.pi) % two_pi - np.pi
|
|
56
|
+
normalized = reference + delta
|
|
57
|
+
|
|
58
|
+
if joint_lower_limits is not None and joint_upper_limits is not None:
|
|
59
|
+
normalized = _prefer_reachable_revolution(
|
|
60
|
+
normalized,
|
|
61
|
+
np.asarray(joint_lower_limits, dtype=float),
|
|
62
|
+
np.asarray(joint_upper_limits, dtype=float),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
result: _JointConfigurationsT = normalized.tolist()
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _prefer_reachable_revolution(
|
|
70
|
+
normalized: "np.ndarray",
|
|
71
|
+
lower_limits: "np.ndarray",
|
|
72
|
+
upper_limits: "np.ndarray",
|
|
73
|
+
) -> "np.ndarray":
|
|
74
|
+
"""Swap out-of-limit angles for their other revolution when that one fits."""
|
|
75
|
+
outside = (normalized < lower_limits) | (normalized > upper_limits)
|
|
76
|
+
if not outside.any():
|
|
77
|
+
return normalized
|
|
78
|
+
|
|
79
|
+
two_pi = 2.0 * np.pi
|
|
80
|
+
alternate = normalized + np.where(normalized < lower_limits, two_pi, -two_pi)
|
|
81
|
+
alternate_fits = (alternate >= lower_limits) & (alternate <= upper_limits)
|
|
82
|
+
|
|
83
|
+
unreachable = outside & ~alternate_fits
|
|
84
|
+
if unreachable.any():
|
|
85
|
+
logger.warning(
|
|
86
|
+
"Joint angles outside limits in both revolutions, leaving them as-is: %s",
|
|
87
|
+
np.asarray(normalized)[unreachable].tolist(),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return np.where(outside & alternate_fits, alternate, normalized)
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Goal normalization must not wind a joint into its hard stop.
|
|
2
|
+
|
|
3
|
+
The degree values below are the ones VRMCS was actually commanded on the Timken
|
|
4
|
+
cell on 2026-08-17, recovered from vrmcs.log. Without limits the normalization
|
|
5
|
+
walked J6 from -117.5 deg to -286.9 deg and then to -362.5 deg, past the UR's
|
|
6
|
+
+/-360 deg travel, and the arm tripped into RECOVERABLE_FAULT mid-trajectory.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from griip_core.utils.joint_math import normalize_joint_configurations_to_reference
|
|
14
|
+
|
|
15
|
+
UR_LOWER = [math.radians(-300.0)] * 6
|
|
16
|
+
UR_UPPER = [math.radians(300.0)] * 6
|
|
17
|
+
# Single-joint cases need single-joint limits; numpy would broadcast a 6-wide
|
|
18
|
+
# limit array against a 1-wide configuration and silently return six angles.
|
|
19
|
+
ONE_JOINT_LOWER = [math.radians(-300.0)]
|
|
20
|
+
ONE_JOINT_UPPER = [math.radians(300.0)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def degrees(joints_rad: list[float]) -> list[float]:
|
|
24
|
+
return [round(math.degrees(value), 1) for value in joints_rad]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def radians(joints_deg: list[float]) -> list[float]:
|
|
28
|
+
return [math.radians(value) for value in joints_deg]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_without_limits_it_still_takes_the_nearest_revolution() -> None:
|
|
32
|
+
"""The documented behaviour when no limits are known, kept intact."""
|
|
33
|
+
goal = radians([73.1])
|
|
34
|
+
reference = radians([-117.5])
|
|
35
|
+
|
|
36
|
+
result = normalize_joint_configurations_to_reference(goal, reference)
|
|
37
|
+
|
|
38
|
+
assert degrees(result) == [-286.9]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_the_capture_move_that_wound_j6() -> None:
|
|
42
|
+
"""home -> apriltag capture: nearest is -286.9, outside +/-300 it must not be used."""
|
|
43
|
+
goal = radians([15.5, -33.0, 72.5, -139.5, -147.6, 73.1])
|
|
44
|
+
reference = radians([60.0, -38.9, 65.4, 245.3, -84.2, -117.5])
|
|
45
|
+
|
|
46
|
+
unlimited = degrees(normalize_joint_configurations_to_reference(goal, reference))
|
|
47
|
+
limited = degrees(
|
|
48
|
+
normalize_joint_configurations_to_reference(goal, reference, joint_lower_limits=UR_LOWER, joint_upper_limits=UR_UPPER)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert unlimited[5] == -286.9
|
|
52
|
+
# -286.9 is inside +/-300, so J6 is left alone; J4's 245.3-relative nearest is not.
|
|
53
|
+
assert limited[5] == -286.9
|
|
54
|
+
assert abs(limited[3]) <= 300.0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_the_calibration_move_that_hit_the_hard_stop() -> None:
|
|
58
|
+
"""capture -> calibration: nearest equivalent is -362.5 deg, past the hard stop."""
|
|
59
|
+
goal = radians([-2.46])
|
|
60
|
+
reference = radians([-286.9])
|
|
61
|
+
|
|
62
|
+
unlimited = degrees(normalize_joint_configurations_to_reference(goal, reference))
|
|
63
|
+
limited = degrees(
|
|
64
|
+
normalize_joint_configurations_to_reference(goal, reference, joint_lower_limits=ONE_JOINT_LOWER, joint_upper_limits=ONE_JOINT_UPPER)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert unlimited == [-362.5], "reproduces the pre-fix command that faulted the arm"
|
|
68
|
+
assert limited == [-2.5], "falls back to the reachable revolution"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_the_reachable_revolution_is_the_same_physical_angle() -> None:
|
|
72
|
+
goal = radians([-2.46])
|
|
73
|
+
reference = radians([-286.9])
|
|
74
|
+
|
|
75
|
+
limited = normalize_joint_configurations_to_reference(
|
|
76
|
+
goal, reference, joint_lower_limits=ONE_JOINT_LOWER, joint_upper_limits=ONE_JOINT_UPPER
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
offset = abs(limited[0] - goal[0])
|
|
80
|
+
assert offset % (2 * math.pi) == pytest.approx(0.0, abs=1e-9)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_in_range_goals_are_untouched() -> None:
|
|
84
|
+
goal = radians([10.0, -20.0, 30.0, -40.0, 50.0, -60.0])
|
|
85
|
+
reference = radians([12.0, -18.0, 33.0, -44.0, 55.0, -66.0])
|
|
86
|
+
|
|
87
|
+
limited = normalize_joint_configurations_to_reference(goal, reference, joint_lower_limits=UR_LOWER, joint_upper_limits=UR_UPPER)
|
|
88
|
+
|
|
89
|
+
assert degrees(limited) == [10.0, -20.0, 30.0, -40.0, 50.0, -60.0]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_a_batch_of_configurations_keeps_its_shape() -> None:
|
|
93
|
+
goals = [radians([-2.46]), radians([73.1])]
|
|
94
|
+
reference = radians([-286.9])
|
|
95
|
+
|
|
96
|
+
limited = normalize_joint_configurations_to_reference(
|
|
97
|
+
goals, reference, joint_lower_limits=[math.radians(-300.0)], joint_upper_limits=[math.radians(300.0)]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert len(limited) == 2
|
|
101
|
+
assert degrees(limited[0]) == [-2.5]
|
|
102
|
+
assert all(abs(math.degrees(value)) <= 300.0 for row in limited for value in row)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_unreachable_in_both_revolutions_is_left_alone_and_warned(caplog: pytest.LogCaptureFixture) -> None:
|
|
106
|
+
"""A goal no revolution can satisfy stays put so the planner can reject it."""
|
|
107
|
+
goal = radians([0.0])
|
|
108
|
+
reference = radians([0.0])
|
|
109
|
+
|
|
110
|
+
with caplog.at_level("WARNING"):
|
|
111
|
+
limited = normalize_joint_configurations_to_reference(
|
|
112
|
+
goal, reference, joint_lower_limits=[math.radians(100.0)], joint_upper_limits=[math.radians(120.0)]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert degrees(limited) == [0.0]
|
|
116
|
+
assert "outside limits in both revolutions" in caplog.text
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""SE3/SO3 transform helpers used internally by the SDK.
|
|
2
|
+
|
|
3
|
+
Kept here (rather than in ``mmai_griip_api.utils.transforms``) so the
|
|
4
|
+
published griip-sdk wheel can be imported without the API app installed.
|
|
5
|
+
``mmai_griip_api.utils.transforms`` re-exports these for backward compat.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import numpy.typing as npt
|
|
12
|
+
from spatialmath import SE3, SO3, UnitQuaternion
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_rotation_matrix_to_so3(rotation: npt.NDArray[np.float64]) -> SO3:
|
|
16
|
+
"""Re-normalize a 3x3 rotation matrix to a valid SO3 via quaternion round-trip."""
|
|
17
|
+
raw_so3 = SO3(rotation, check=False)
|
|
18
|
+
quaternion_xyzs = raw_so3.UnitQuaternion().vec_xyzs
|
|
19
|
+
x, y, z, scalar = quaternion_xyzs / np.linalg.norm(quaternion_xyzs)
|
|
20
|
+
return UnitQuaternion(s=scalar, v=[x, y, z]).SO3()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalize_rotation_matrix_to_se3(pose: npt.NDArray[np.float64]) -> SE3:
|
|
24
|
+
"""Build an SE3 from a raw 4x4 matrix, re-normalizing the rotation to be a valid SO3."""
|
|
25
|
+
rotation = normalize_rotation_matrix_to_so3(pose[:3, :3])
|
|
26
|
+
translation = pose[:3, 3]
|
|
27
|
+
return SE3.Rt(R=rotation, t=translation)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def se3_to_pose_list(pose: SE3) -> list[float]:
|
|
31
|
+
"""Convert SE3 to [x, y, z, qw, qx, qy, qz] format for IK solving."""
|
|
32
|
+
translation = pose.t
|
|
33
|
+
quaternion_xyzs = UnitQuaternion(pose.R).vec_xyzs
|
|
34
|
+
return [
|
|
35
|
+
float(translation[0]),
|
|
36
|
+
float(translation[1]),
|
|
37
|
+
float(translation[2]),
|
|
38
|
+
float(quaternion_xyzs[3]),
|
|
39
|
+
float(quaternion_xyzs[0]),
|
|
40
|
+
float(quaternion_xyzs[1]),
|
|
41
|
+
float(quaternion_xyzs[2]),
|
|
42
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: griip-core-dev
|
|
3
|
+
Version: 0.5.6.dev25546
|
|
4
|
+
Summary: Dev build of griip-core from commit 0af7ee671 (PR #6192). Not a release.
|
|
5
|
+
Author: VentionCo
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: <3.13,>=3.10
|
|
8
|
+
Requires-Dist: grpcio>=1.78.0
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: protobuf>=4.0
|
|
11
|
+
Requires-Dist: spatialmath-python<2.0.0,>=1.1.15
|
|
12
|
+
Requires-Dist: vention-grpc-py-sdk>=0.0.72
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
griip_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
griip_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
griip_core/robot_drivers.py,sha256=koVucKBYTaI1ve7PSxUQhuBcdlj7kBOeSsBTENc9e8Q,328
|
|
4
|
+
griip_core/generated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
griip_core/generated/griip_pb2.py,sha256=cn3UZ_3fkaAcDtNzQVc5DqxnJzeEemdVJNBPZkalwYI,22782
|
|
6
|
+
griip_core/generated/griip_pb2.pyi,sha256=26QhSFqaElTEQSPLYZ7sXRyebZemJxDiQm8kwIryd-Q,39704
|
|
7
|
+
griip_core/generated/griip_pb2_grpc.py,sha256=sdjto2CXVNDk0GU4GkRi5T5UhjpKeKnsq-byNFfzj4U,27263
|
|
8
|
+
griip_core/generated/griip_pb2_grpc.pyi,sha256=YsnkviRAliqP1f-jRk7pC_t8ai88UefUYBQ7sB4mYAY,20330
|
|
9
|
+
griip_core/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
griip_core/hardware/vrmcs_client.py,sha256=Ks1j3L8czl1S5O_YmKvVQKZPCz8ZnWWHP0Xv9l0Hau4,54216
|
|
11
|
+
griip_core/hardware/vrmcs_client.pyi,sha256=qzIlnh1h3f1e8wbMxyiSj8VA7CuIOvbD6Uu8TwSA_fo,8215
|
|
12
|
+
griip_core/hardware/vrmcs_registration.py,sha256=7lwtyyGlcJgVx9sGPMU2RwKcYN0vTAgT3RryH4we_pA,5239
|
|
13
|
+
griip_core/hardware/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
griip_core/hardware/tests/test_vrmcs_registration.py,sha256=75uBoWTyNGXyr2cABjwBJrMqoD4ujBEJpC8JuMClRl8,6325
|
|
15
|
+
griip_core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
griip_core/utils/joint_math.py,sha256=KYLXGpiPr8qx36JT0gSh77czTvqrBw3iFoKJgsw6DxU,3373
|
|
17
|
+
griip_core/utils/transforms.py,sha256=SlFwNhYTq2LmpaLP8aiw7h5F1PIHyvxDAX1qPlXfq5g,1567
|
|
18
|
+
griip_core/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
griip_core/utils/tests/test_joint_math.py,sha256=P0d3eFhj-x8ebTGoCRkY5ZNmgh_Td00cWmDSKVIUbug,4548
|
|
20
|
+
griip_core_dev-0.5.6.dev25546.dist-info/METADATA,sha256=1GNlcFLl4IEIr8oWi9RMkvHwPAavWLBvJgNRrRFMMzQ,396
|
|
21
|
+
griip_core_dev-0.5.6.dev25546.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
22
|
+
griip_core_dev-0.5.6.dev25546.dist-info/RECORD,,
|