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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any