crisp-python 1.4.0__tar.gz → 1.5.0__tar.gz
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.
- {crisp_python-1.4.0/crisp_python.egg-info → crisp_python-1.5.0}/PKG-INFO +1 -1
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/robot.py +46 -62
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/robot_config.py +1 -0
- crisp_python-1.5.0/crisp_py/utils/geometry.py +107 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0/crisp_python.egg-info}/PKG-INFO +1 -1
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_python.egg-info/SOURCES.txt +1 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/pyproject.toml +1 -1
- {crisp_python-1.4.0 → crisp_python-1.5.0}/LICENSE.md +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/MANIFEST.in +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/README.md +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/__init__.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/camera/__init__.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/camera/camera.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/camera/camera_config.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/clipped_cartesian_impedance.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/default_cartesian_impedance.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/default_operational_space_controller.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation_on_plane.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/joint_control.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/joint_velocity_control.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/no_friction_cartesian_impedance.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/gripper_franka.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/gripper_left.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/gripper_left_config.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/gripper_right.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/gripper_right_config.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/gripper_uncalibrated.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/gripper_with_trigger.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/trigger_left.yaml +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/control/__init__.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/control/controller_switcher.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/control/joint_trajectory_controller_client.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/control/parameters_client.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/gripper/__init__.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/gripper/gripper.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/sensors/sensor.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/sensors/sensor_config.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/utils/__init__.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/utils/callback_monitor.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/utils/diagnostics_viewer.py +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_python.egg-info/dependency_links.txt +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_python.egg-info/requires.txt +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_python.egg-info/top_level.txt +0 -0
- {crisp_python-1.4.0 → crisp_python-1.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crisp-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Simple python interface to control robots that use crisp_controllers and other ROS2 components.
|
|
5
5
|
Project-URL: Homepage, https://utiasDSL.github.io/crisp_controllers/
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""Provides a client to control the franka robot. It is the easiest way to control the robot using ROS2."""
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
|
-
from dataclasses import dataclass
|
|
5
4
|
from typing import List
|
|
6
5
|
|
|
7
6
|
import numpy as np
|
|
8
7
|
import rclpy
|
|
9
8
|
import rclpy.executors
|
|
10
|
-
from geometry_msgs.msg import PoseStamped, WrenchStamped
|
|
9
|
+
from geometry_msgs.msg import PoseStamped, TwistStamped, WrenchStamped
|
|
11
10
|
from numpy.typing import NDArray
|
|
12
11
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
|
13
12
|
from rclpy.node import Node
|
|
@@ -20,36 +19,7 @@ from crisp_py.control.joint_trajectory_controller_client import JointTrajectoryC
|
|
|
20
19
|
from crisp_py.control.parameters_client import ParametersClient
|
|
21
20
|
from crisp_py.robot_config import FrankaConfig, RobotConfig
|
|
22
21
|
from crisp_py.utils.callback_monitor import CallbackMonitor
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class Pose:
|
|
27
|
-
"""Compact representation of an SE3 object."""
|
|
28
|
-
|
|
29
|
-
position: np.ndarray
|
|
30
|
-
orientation: Rotation
|
|
31
|
-
|
|
32
|
-
def copy(self) -> "Pose":
|
|
33
|
-
"""Create a copy of this pose."""
|
|
34
|
-
return Pose(self.position.copy(), Rotation.from_quat(self.orientation.as_quat()))
|
|
35
|
-
|
|
36
|
-
def __str__(self) -> str:
|
|
37
|
-
"""Return a string representation of a Pose."""
|
|
38
|
-
return f"Pos: {np.array2string(self.position, suppress_small=True, precision=2, floatmode='fixed')},\n Orientation: {np.array2string(self.orientation.as_matrix(), suppress_small=True, precision=2, floatmode='fixed')}"
|
|
39
|
-
|
|
40
|
-
def __sub__(self, other: "Pose") -> "Pose":
|
|
41
|
-
"""Subtract another pose from this pose, i.e. compute the relative pose."""
|
|
42
|
-
return Pose(
|
|
43
|
-
self.position - other.position,
|
|
44
|
-
self.orientation * other.orientation.inv(),
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
def __add__(self, other: "Pose") -> "Pose":
|
|
48
|
-
"""Add another pose to this pose, i.e. add a relative pose."""
|
|
49
|
-
return Pose(
|
|
50
|
-
self.position + other.position,
|
|
51
|
-
other.orientation * self.orientation,
|
|
52
|
-
)
|
|
22
|
+
from crisp_py.utils.geometry import Pose, Twist
|
|
53
23
|
|
|
54
24
|
|
|
55
25
|
class Robot:
|
|
@@ -111,6 +81,7 @@ class Robot:
|
|
|
111
81
|
self._current_joint = None
|
|
112
82
|
self._target_joint = None
|
|
113
83
|
self._target_wrench = None
|
|
84
|
+
self._current_twist = None
|
|
114
85
|
|
|
115
86
|
self._callback_monitor = CallbackMonitor(
|
|
116
87
|
node=self.node,
|
|
@@ -145,6 +116,16 @@ class Robot:
|
|
|
145
116
|
callback_group=ReentrantCallbackGroup(),
|
|
146
117
|
)
|
|
147
118
|
|
|
119
|
+
self.node.create_subscription(
|
|
120
|
+
TwistStamped,
|
|
121
|
+
self.config.current_twist_topic,
|
|
122
|
+
self._callback_monitor.monitor(
|
|
123
|
+
f"{namespace.capitalize()} Current Twist", self._callback_current_twist
|
|
124
|
+
),
|
|
125
|
+
qos_profile_sensor_data,
|
|
126
|
+
callback_group=ReentrantCallbackGroup(),
|
|
127
|
+
)
|
|
128
|
+
|
|
148
129
|
self.node.create_timer(
|
|
149
130
|
1.0 / self.config.publish_frequency,
|
|
150
131
|
self._callback_monitor.monitor(
|
|
@@ -237,6 +218,22 @@ class Robot:
|
|
|
237
218
|
)
|
|
238
219
|
return self._target_joint.copy()
|
|
239
220
|
|
|
221
|
+
@property
|
|
222
|
+
def end_effector_twist(self) -> Twist:
|
|
223
|
+
"""Get the current twist of the end effector.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Twist: Current end-effector twist message.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
RuntimeError: If no twist messages have been received yet.
|
|
230
|
+
"""
|
|
231
|
+
if self._current_twist is None:
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
f"The robot has not received any twists yet. Is the current_twist_topic {self.config.current_twist_topic} correct?"
|
|
234
|
+
)
|
|
235
|
+
return self._current_twist.copy()
|
|
236
|
+
|
|
240
237
|
def is_ready(self) -> bool:
|
|
241
238
|
"""Check if the robot is ready for operation.
|
|
242
239
|
|
|
@@ -311,7 +308,11 @@ class Robot:
|
|
|
311
308
|
"""
|
|
312
309
|
if self._target_pose is None or not rclpy.ok():
|
|
313
310
|
return
|
|
314
|
-
self._target_pose_publisher.publish(
|
|
311
|
+
self._target_pose_publisher.publish(
|
|
312
|
+
self._target_pose.to_ros_msg(
|
|
313
|
+
self.config.base_frame, self.node.get_clock().now().to_msg()
|
|
314
|
+
)
|
|
315
|
+
)
|
|
315
316
|
|
|
316
317
|
def _callback_publish_target_joint(self):
|
|
317
318
|
"""Publish the current target joint configuration if one exists.
|
|
@@ -384,10 +385,21 @@ class Robot:
|
|
|
384
385
|
Args:
|
|
385
386
|
msg (PoseStamped): ROS message containing the current pose.
|
|
386
387
|
"""
|
|
387
|
-
self._current_pose =
|
|
388
|
+
self._current_pose = Pose.from_ros_msg(msg)
|
|
388
389
|
if self._target_pose is None:
|
|
389
390
|
self._target_pose = self._current_pose.copy()
|
|
390
391
|
|
|
392
|
+
def _callback_current_twist(self, msg: TwistStamped):
|
|
393
|
+
"""Update the current twist from a ROS message.
|
|
394
|
+
|
|
395
|
+
This callback is triggered when a new twist message is received. It updates
|
|
396
|
+
the current twist.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
msg (TwistStamped): ROS message containing the current twist.
|
|
400
|
+
"""
|
|
401
|
+
self._current_twist = Twist.from_ros_msg(msg)
|
|
402
|
+
|
|
391
403
|
def _callback_current_joint(self, msg: JointState):
|
|
392
404
|
"""Update the current joint state from a ROS message.
|
|
393
405
|
|
|
@@ -467,34 +479,6 @@ class Robot:
|
|
|
467
479
|
# if switch_to_default_controller:
|
|
468
480
|
# self.controller_switcher_client.switch_controller(self.config.default_controller)
|
|
469
481
|
|
|
470
|
-
def _pose_msg_to_pose(self, pose: PoseStamped) -> Pose:
|
|
471
|
-
"""Convert a ROS2 pose msg to a pose."""
|
|
472
|
-
position = np.array([pose.pose.position.x, pose.pose.position.y, pose.pose.position.z])
|
|
473
|
-
orientation = Rotation.from_quat(
|
|
474
|
-
[
|
|
475
|
-
pose.pose.orientation.x,
|
|
476
|
-
pose.pose.orientation.y,
|
|
477
|
-
pose.pose.orientation.z,
|
|
478
|
-
pose.pose.orientation.w,
|
|
479
|
-
]
|
|
480
|
-
)
|
|
481
|
-
return Pose(position, orientation)
|
|
482
|
-
|
|
483
|
-
def _pose_to_pose_msg(self, pose: Pose) -> PoseStamped:
|
|
484
|
-
"""Convert a pose to a pose message."""
|
|
485
|
-
msg = PoseStamped()
|
|
486
|
-
msg.header.frame_id = self.config.base_frame
|
|
487
|
-
msg.header.stamp = self.node.get_clock().now().to_msg()
|
|
488
|
-
msg.pose.position.x, msg.pose.position.y, msg.pose.position.z = pose.position
|
|
489
|
-
q = pose.orientation.as_quat()
|
|
490
|
-
(
|
|
491
|
-
msg.pose.orientation.x,
|
|
492
|
-
msg.pose.orientation.y,
|
|
493
|
-
msg.pose.orientation.z,
|
|
494
|
-
msg.pose.orientation.w,
|
|
495
|
-
) = q
|
|
496
|
-
return msg
|
|
497
|
-
|
|
498
482
|
def _joint_to_joint_msg(
|
|
499
483
|
self, q: NDArray, dq: NDArray | None = None, tau: NDArray | None = None
|
|
500
484
|
) -> JointState:
|
|
@@ -41,6 +41,7 @@ class RobotConfig:
|
|
|
41
41
|
target_joint_topic: str = "target_joint"
|
|
42
42
|
current_pose_topic: str = "current_pose"
|
|
43
43
|
current_joint_topic: str = "joint_states"
|
|
44
|
+
current_twist_topic: str = "current_twist"
|
|
44
45
|
|
|
45
46
|
publish_frequency: float = 50.0
|
|
46
47
|
time_to_home: float = 5.0
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Utility functions for geometry operations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from builtin_interfaces.msg import Time as TimeMsg
|
|
7
|
+
from geometry_msgs.msg import PoseStamped, TwistStamped
|
|
8
|
+
from scipy.spatial.transform import Rotation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Pose:
|
|
13
|
+
"""Compact representation of an SE3 object."""
|
|
14
|
+
|
|
15
|
+
position: np.ndarray
|
|
16
|
+
orientation: Rotation
|
|
17
|
+
|
|
18
|
+
def copy(self) -> "Pose":
|
|
19
|
+
"""Create a copy of this pose."""
|
|
20
|
+
return Pose(self.position.copy(), Rotation.from_quat(self.orientation.as_quat()))
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
"""Return a string representation of a Pose."""
|
|
24
|
+
return f"Pos: {np.array2string(self.position, suppress_small=True, precision=2, floatmode='fixed')},\n Orientation: {np.array2string(self.orientation.as_matrix(), suppress_small=True, precision=2, floatmode='fixed')}"
|
|
25
|
+
|
|
26
|
+
def __sub__(self, other: "Pose") -> "Pose":
|
|
27
|
+
"""Subtract another pose from this pose, i.e. compute the relative pose."""
|
|
28
|
+
return Pose(
|
|
29
|
+
self.position - other.position,
|
|
30
|
+
self.orientation * other.orientation.inv(),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __add__(self, other: "Pose") -> "Pose":
|
|
34
|
+
"""Add another pose to this pose, i.e. add a relative pose."""
|
|
35
|
+
return Pose(
|
|
36
|
+
self.position + other.position,
|
|
37
|
+
other.orientation * self.orientation,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_ros_msg(cls, msg: PoseStamped) -> "Pose":
|
|
42
|
+
"""Create Pose from ROS PoseStamped message."""
|
|
43
|
+
position = np.array([msg.pose.position.x, msg.pose.position.y, msg.pose.position.z])
|
|
44
|
+
orientation = Rotation.from_quat(
|
|
45
|
+
[
|
|
46
|
+
msg.pose.orientation.x,
|
|
47
|
+
msg.pose.orientation.y,
|
|
48
|
+
msg.pose.orientation.z,
|
|
49
|
+
msg.pose.orientation.w,
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
return cls(position, orientation)
|
|
53
|
+
|
|
54
|
+
def to_ros_msg(self, frame_id: str, stamp: TimeMsg) -> PoseStamped:
|
|
55
|
+
"""Convert Pose to ROS PoseStamped message."""
|
|
56
|
+
msg = PoseStamped()
|
|
57
|
+
msg.header.frame_id = frame_id
|
|
58
|
+
msg.header.stamp = stamp
|
|
59
|
+
msg.pose.position.x, msg.pose.position.y, msg.pose.position.z = self.position
|
|
60
|
+
q = self.orientation.as_quat()
|
|
61
|
+
(
|
|
62
|
+
msg.pose.orientation.x,
|
|
63
|
+
msg.pose.orientation.y,
|
|
64
|
+
msg.pose.orientation.z,
|
|
65
|
+
msg.pose.orientation.w,
|
|
66
|
+
) = q
|
|
67
|
+
return msg
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class Twist:
|
|
72
|
+
"""Compact representation of a twist (linear and angular velocity)."""
|
|
73
|
+
|
|
74
|
+
linear: np.ndarray
|
|
75
|
+
angular: np.ndarray
|
|
76
|
+
|
|
77
|
+
def copy(self) -> "Twist":
|
|
78
|
+
"""Create a copy of this twist."""
|
|
79
|
+
return Twist(self.linear.copy(), self.angular.copy())
|
|
80
|
+
|
|
81
|
+
def __str__(self) -> str:
|
|
82
|
+
"""Return a string representation of a Twist."""
|
|
83
|
+
return f"Linear: {np.array2string(self.linear, suppress_small=True, precision=2, floatmode='fixed')},\n Angular: {np.array2string(self.angular, suppress_small=True, precision=2, floatmode='fixed')}"
|
|
84
|
+
|
|
85
|
+
def magnitude(self) -> float:
|
|
86
|
+
"""Return the magnitude of the linear velocity."""
|
|
87
|
+
return np.linalg.norm(self.linear).astype(float)
|
|
88
|
+
|
|
89
|
+
def angular_magnitude(self) -> float:
|
|
90
|
+
"""Return the magnitude of the angular velocity."""
|
|
91
|
+
return np.linalg.norm(self.angular).astype(float)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_ros_msg(cls, msg: TwistStamped) -> "Twist":
|
|
95
|
+
"""Create Twist from ROS TwistStamped message."""
|
|
96
|
+
linear = np.array([msg.twist.linear.x, msg.twist.linear.y, msg.twist.linear.z])
|
|
97
|
+
angular = np.array([msg.twist.angular.x, msg.twist.angular.y, msg.twist.angular.z])
|
|
98
|
+
return cls(linear, angular)
|
|
99
|
+
|
|
100
|
+
def to_ros_msg(self, frame_id: str, stamp: TimeMsg) -> TwistStamped:
|
|
101
|
+
"""Convert Twist to ROS TwistStamped message."""
|
|
102
|
+
msg = TwistStamped()
|
|
103
|
+
msg.header.frame_id = frame_id
|
|
104
|
+
msg.header.stamp = stamp
|
|
105
|
+
msg.twist.linear.x, msg.twist.linear.y, msg.twist.linear.z = self.linear
|
|
106
|
+
msg.twist.angular.x, msg.twist.angular.y, msg.twist.angular.z = self.angular
|
|
107
|
+
return msg
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crisp-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Simple python interface to control robots that use crisp_controllers and other ROS2 components.
|
|
5
5
|
Project-URL: Homepage, https://utiasDSL.github.io/crisp_controllers/
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -35,6 +35,7 @@ crisp_py/sensors/sensor_config.py
|
|
|
35
35
|
crisp_py/utils/__init__.py
|
|
36
36
|
crisp_py/utils/callback_monitor.py
|
|
37
37
|
crisp_py/utils/diagnostics_viewer.py
|
|
38
|
+
crisp_py/utils/geometry.py
|
|
38
39
|
crisp_python.egg-info/PKG-INFO
|
|
39
40
|
crisp_python.egg-info/SOURCES.txt
|
|
40
41
|
crisp_python.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/clipped_cartesian_impedance.yaml
RENAMED
|
File without changes
|
{crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/default_cartesian_impedance.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/gravity_compensation_on_plane.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/config/control/joint_velocity_control.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{crisp_python-1.4.0 → crisp_python-1.5.0}/crisp_py/control/joint_trajectory_controller_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|