urkit 0.3.18__tar.gz → 0.3.20__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.
- {urkit-0.3.18 → urkit-0.3.20}/PKG-INFO +39 -1
- {urkit-0.3.18 → urkit-0.3.20}/README.md +38 -0
- {urkit-0.3.18 → urkit-0.3.20}/pyproject.toml +1 -1
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/__init__.py +1 -1
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/cli/teach.py +22 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/config.py +1 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/robot.py +363 -38
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit.egg-info/PKG-INFO +39 -1
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit.egg-info/SOURCES.txt +1 -0
- urkit-0.3.20/tests/test_move_sequence.py +296 -0
- {urkit-0.3.18 → urkit-0.3.20}/setup.cfg +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/__main__.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/cli/__init__.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/cli/colors.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/cli/connection_monitor.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/cli/points.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/connection.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/exceptions.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/geometry.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/gripper/__init__.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/gripper/base.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/gripper/digital.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/gripper/presets.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/gripper/robotiq.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/gripper/robotiq_preamble.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/io.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/motion.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/points.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit/telemetry.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit.egg-info/dependency_links.txt +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit.egg-info/entry_points.txt +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit.egg-info/requires.txt +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/src/urkit.egg-info/top_level.txt +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/tests/test_exceptions.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/tests/test_geometry.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/tests/test_gripper.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/tests/test_gripper_factory.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/tests/test_gripper_presets.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/tests/test_points.py +0 -0
- {urkit-0.3.18 → urkit-0.3.20}/tests/test_robot_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.20
|
|
4
4
|
Summary: Universal Robots e-Series control toolkit built on ur_rtde
|
|
5
5
|
Author: URKit Contributors
|
|
6
6
|
License: MIT
|
|
@@ -424,6 +424,43 @@ robot.move_relative(delta_z=0.05) # 5cm along tool Z
|
|
|
424
424
|
- **BASE** (default): delta relative to robot base
|
|
425
425
|
- **TOOL**: delta relative to TCP orientation
|
|
426
426
|
|
|
427
|
+
#### IK Reference (recommended)
|
|
428
|
+
|
|
429
|
+
**Problem:** When the robot has multiple valid joint configurations to reach the same TCP pose (e.g., elbow up vs. elbow down), it can unexpectedly flip its posture between moves. This is called an **IK ambiguity** and it causes "weird" movements where the robot takes a strange path or flips its wrist.
|
|
430
|
+
|
|
431
|
+
**Solution:** Set an **IK reference posture** — a saved point that defines your preferred arm configuration. The robot then stays close to that posture for all moves.
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
# 1. Put the robot in your preferred posture (e.g., "home")
|
|
435
|
+
# 2. Save it: robot.save_point("home")
|
|
436
|
+
# 3. Set as IK reference:
|
|
437
|
+
robot.ik_reference = "home"
|
|
438
|
+
|
|
439
|
+
# Now all moves stay close to that posture — no elbow flipping
|
|
440
|
+
robot.move_to("pick")
|
|
441
|
+
robot.move_to("place")
|
|
442
|
+
robot.move_relative(delta_z=-0.05)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**In config.yaml** (recommended for permanent setup):
|
|
446
|
+
|
|
447
|
+
```yaml
|
|
448
|
+
robot_ip: 192.168.1.50
|
|
449
|
+
ik_reference: home # prevents weird elbow/wrist flips
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**How it works:** The robot's inverse kinematics solver uses the reference posture as a bias (`qnear`). The TCP still reaches the exact same pose, but the arm configuration (elbow up/down, wrist orientation) stays consistent with your reference.
|
|
453
|
+
|
|
454
|
+
**Per-move override:**
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
robot.ik_reference = "home" # global default
|
|
458
|
+
robot.move_to("weird_pose", ik_reference=None) # one move without it
|
|
459
|
+
robot.move_to("back", ik_reference="current") # use current joints
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**When to use it:** Almost always. If you've ever seen the robot move in a way that looked "wrong" or flipped its elbow unexpectedly, this is what fixes it. Set it once in config.yaml and forget about it.
|
|
463
|
+
|
|
427
464
|
#### Points are tool-agnostic
|
|
428
465
|
|
|
429
466
|
Points are stored in the active TCP frame, so they work with any tool. If you swap grippers and set the correct TCP offset, your saved points remain valid.
|
|
@@ -589,6 +626,7 @@ URKit searches for `config.yaml` in this order:
|
|
|
589
626
|
| `robot_ip` | Robot IP address | `192.168.1.50` |
|
|
590
627
|
| `points_path` | Path to SQLite points database | `points.db` |
|
|
591
628
|
| `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `digital` |
|
|
629
|
+
| `ik_reference` | IK reference posture (prevents elbow flipping) | `home` |
|
|
592
630
|
| `default_vel` | Default linear velocity (m/s) | `0.5` |
|
|
593
631
|
| `default_acc` | Default linear acceleration (m/s²) | `0.3` |
|
|
594
632
|
| `expert_mode` | Disable safety speed clamping | `false` |
|
|
@@ -398,6 +398,43 @@ robot.move_relative(delta_z=0.05) # 5cm along tool Z
|
|
|
398
398
|
- **BASE** (default): delta relative to robot base
|
|
399
399
|
- **TOOL**: delta relative to TCP orientation
|
|
400
400
|
|
|
401
|
+
#### IK Reference (recommended)
|
|
402
|
+
|
|
403
|
+
**Problem:** When the robot has multiple valid joint configurations to reach the same TCP pose (e.g., elbow up vs. elbow down), it can unexpectedly flip its posture between moves. This is called an **IK ambiguity** and it causes "weird" movements where the robot takes a strange path or flips its wrist.
|
|
404
|
+
|
|
405
|
+
**Solution:** Set an **IK reference posture** — a saved point that defines your preferred arm configuration. The robot then stays close to that posture for all moves.
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
# 1. Put the robot in your preferred posture (e.g., "home")
|
|
409
|
+
# 2. Save it: robot.save_point("home")
|
|
410
|
+
# 3. Set as IK reference:
|
|
411
|
+
robot.ik_reference = "home"
|
|
412
|
+
|
|
413
|
+
# Now all moves stay close to that posture — no elbow flipping
|
|
414
|
+
robot.move_to("pick")
|
|
415
|
+
robot.move_to("place")
|
|
416
|
+
robot.move_relative(delta_z=-0.05)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**In config.yaml** (recommended for permanent setup):
|
|
420
|
+
|
|
421
|
+
```yaml
|
|
422
|
+
robot_ip: 192.168.1.50
|
|
423
|
+
ik_reference: home # prevents weird elbow/wrist flips
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**How it works:** The robot's inverse kinematics solver uses the reference posture as a bias (`qnear`). The TCP still reaches the exact same pose, but the arm configuration (elbow up/down, wrist orientation) stays consistent with your reference.
|
|
427
|
+
|
|
428
|
+
**Per-move override:**
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
robot.ik_reference = "home" # global default
|
|
432
|
+
robot.move_to("weird_pose", ik_reference=None) # one move without it
|
|
433
|
+
robot.move_to("back", ik_reference="current") # use current joints
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**When to use it:** Almost always. If you've ever seen the robot move in a way that looked "wrong" or flipped its elbow unexpectedly, this is what fixes it. Set it once in config.yaml and forget about it.
|
|
437
|
+
|
|
401
438
|
#### Points are tool-agnostic
|
|
402
439
|
|
|
403
440
|
Points are stored in the active TCP frame, so they work with any tool. If you swap grippers and set the correct TCP offset, your saved points remain valid.
|
|
@@ -563,6 +600,7 @@ URKit searches for `config.yaml` in this order:
|
|
|
563
600
|
| `robot_ip` | Robot IP address | `192.168.1.50` |
|
|
564
601
|
| `points_path` | Path to SQLite points database | `points.db` |
|
|
565
602
|
| `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `digital` |
|
|
603
|
+
| `ik_reference` | IK reference posture (prevents elbow flipping) | `home` |
|
|
566
604
|
| `default_vel` | Default linear velocity (m/s) | `0.5` |
|
|
567
605
|
| `default_acc` | Default linear acceleration (m/s²) | `0.3` |
|
|
568
606
|
| `expert_mode` | Disable safety speed clamping | `false` |
|
|
@@ -738,9 +738,17 @@ def _submenu_goto_point(
|
|
|
738
738
|
time.sleep(0.05)
|
|
739
739
|
|
|
740
740
|
# Show dedicated moving screen with monotonic progress
|
|
741
|
+
# Timeout after 60s in case is_moving() stalls (heavy robot settling,
|
|
742
|
+
# robot already at target, etc.)
|
|
741
743
|
cancelled = False
|
|
744
|
+
timed_out = False
|
|
742
745
|
max_progress = 0.0
|
|
746
|
+
move_start = time.monotonic()
|
|
743
747
|
while robot.is_moving():
|
|
748
|
+
if time.monotonic() - move_start > 60.0:
|
|
749
|
+
timed_out = True
|
|
750
|
+
break
|
|
751
|
+
|
|
744
752
|
pct, bar = _draw_moving_screen(
|
|
745
753
|
robot, name, mode_label, start_pose, target_pose, is_cartesian, max_progress
|
|
746
754
|
)
|
|
@@ -758,6 +766,11 @@ def _submenu_goto_point(
|
|
|
758
766
|
if cancelled:
|
|
759
767
|
robot.stop()
|
|
760
768
|
messages.append(f"Move to '{name}' cancelled")
|
|
769
|
+
elif timed_out:
|
|
770
|
+
messages.append(
|
|
771
|
+
f"Moved to '{name}' ({mode_label}) — "
|
|
772
|
+
"timeout, robot may already have been at target"
|
|
773
|
+
)
|
|
761
774
|
else:
|
|
762
775
|
messages.append(f"Moved to '{name}' ({mode_label})")
|
|
763
776
|
except URKitConnectionError:
|
|
@@ -1373,6 +1386,12 @@ def teach_command(args) -> None:
|
|
|
1373
1386
|
if gripper_name and isinstance(gripper_name, str):
|
|
1374
1387
|
gripper_name = gripper_name.lower().replace("_", "-")
|
|
1375
1388
|
points_path = args.points or config.get("points_path") or "points.db"
|
|
1389
|
+
raw_ik = config.get("ik_reference")
|
|
1390
|
+
ik_reference: str | list[float] | None = None
|
|
1391
|
+
if isinstance(raw_ik, str):
|
|
1392
|
+
ik_reference = raw_ik
|
|
1393
|
+
elif isinstance(raw_ik, list) and len(raw_ik) == 6:
|
|
1394
|
+
ik_reference = raw_ik
|
|
1376
1395
|
|
|
1377
1396
|
# Resolve gripper constructor params from config.yaml gripper_config section
|
|
1378
1397
|
# and CLI --gripper-* flags (CLI overrides config)
|
|
@@ -1441,6 +1460,8 @@ def teach_command(args) -> None:
|
|
|
1441
1460
|
if gripper_name:
|
|
1442
1461
|
print(f" Gripper: {gripper_name}")
|
|
1443
1462
|
print(f" Points: {points_path}")
|
|
1463
|
+
if ik_reference:
|
|
1464
|
+
print(f" IK ref: {ik_reference}")
|
|
1444
1465
|
|
|
1445
1466
|
# URRobot handles everything: safety recovery, remote mode check,
|
|
1446
1467
|
# power on, brake release, program stop, and RTDE connection.
|
|
@@ -1449,6 +1470,7 @@ def teach_command(args) -> None:
|
|
|
1449
1470
|
ip=ip, # type: ignore
|
|
1450
1471
|
points=points_path, # type: ignore
|
|
1451
1472
|
gripper=gripper_config,
|
|
1473
|
+
ik_reference=ik_reference,
|
|
1452
1474
|
**gripper_kwargs,
|
|
1453
1475
|
)
|
|
1454
1476
|
print(" Connected.", flush=True)
|
|
@@ -10,7 +10,7 @@ from dataclasses import replace
|
|
|
10
10
|
import logging
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
13
|
+
from typing import TYPE_CHECKING, Literal, Optional, Union
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
@@ -89,6 +89,26 @@ class URRobot:
|
|
|
89
89
|
Presets provide mass, CoG, TCP offset, and backend type.
|
|
90
90
|
default_vel: Default linear velocity (m/s).
|
|
91
91
|
default_acc: Default linear acceleration (m/s²).
|
|
92
|
+
ik_reference: Global reference posture for inverse kinematics.
|
|
93
|
+
One of:
|
|
94
|
+
|
|
95
|
+
- ``None`` (default): No IK reference — the controller picks
|
|
96
|
+
the IK solution closest to the current joint positions.
|
|
97
|
+
- ``"current"``: Use the robot's current joint positions
|
|
98
|
+
as the reference.
|
|
99
|
+
- A point name (str): Look up the saved point, resolve its
|
|
100
|
+
pose to joints, and use as the reference. If the point
|
|
101
|
+
doesn't exist, logs a warning and falls back to current
|
|
102
|
+
joint positions.
|
|
103
|
+
- A 6-element list: Use directly as joint angles.
|
|
104
|
+
|
|
105
|
+
When set, all motion commands (``move_to``, ``move_relative``,
|
|
106
|
+
``move_sequence``) resolve target poses to joint angles using
|
|
107
|
+
this reference, preventing the robot from unexpectedly flipping
|
|
108
|
+
its elbow or wrist. Can be overridden per-call by passing
|
|
109
|
+
``ik_reference`` to individual motion methods.
|
|
110
|
+
|
|
111
|
+
Can be changed at runtime via the ``ik_reference`` property.
|
|
92
112
|
gripper_kwargs: Additional kwargs passed to the gripper backend
|
|
93
113
|
to override preset values (e.g. ``max_mm=80`` for custom
|
|
94
114
|
fingers, ``force=50``, ``speed=80`` for Robotiq).
|
|
@@ -99,6 +119,8 @@ class URRobot:
|
|
|
99
119
|
>>> robot.move_relative([0.01, 0, 0, 0, 0, 0]) # works without points
|
|
100
120
|
>>> robot.points_db = "points.db" # set lazily
|
|
101
121
|
>>> robot.save_point("home")
|
|
122
|
+
>>> robot.ik_reference = "home" # prevent elbow flipping
|
|
123
|
+
>>> robot.move_to("pick") # uses home as IK reference
|
|
102
124
|
"""
|
|
103
125
|
|
|
104
126
|
def __init__(
|
|
@@ -109,6 +131,7 @@ class URRobot:
|
|
|
109
131
|
gripper: GripperPreset | DigitalGripperConfig | None = None,
|
|
110
132
|
default_vel: float = 0.5,
|
|
111
133
|
default_acc: float = 0.3,
|
|
134
|
+
ik_reference: str | list[float] | None = None,
|
|
112
135
|
**gripper_kwargs: object,
|
|
113
136
|
) -> None:
|
|
114
137
|
self._ip = ip
|
|
@@ -117,6 +140,11 @@ class URRobot:
|
|
|
117
140
|
self._rtde_frequency = 500.0
|
|
118
141
|
self._connection_lost = False
|
|
119
142
|
self._move_frame: MoveFrame = MoveFrame.BASE
|
|
143
|
+
self._ik_reference: str | list[float] | None = ik_reference
|
|
144
|
+
|
|
145
|
+
# Target tracking for pose-based arrival detection in is_moving()
|
|
146
|
+
self._move_target_pose: list[float] | None = None
|
|
147
|
+
self._move_target_joints: list[float] | None = None
|
|
120
148
|
|
|
121
149
|
# Points database (internal) — optional, lazy-initialized
|
|
122
150
|
self._points: Points | None = Points(points) if points is not None else None
|
|
@@ -348,6 +376,7 @@ class URRobot:
|
|
|
348
376
|
gripper: GripperPreset | DigitalGripperConfig | str | None = None,
|
|
349
377
|
default_vel: float | None = None,
|
|
350
378
|
default_acc: float | None = None,
|
|
379
|
+
ik_reference: str | list[float] | None = None,
|
|
351
380
|
**gripper_kwargs,
|
|
352
381
|
) -> "URRobot":
|
|
353
382
|
"""Create a URRobot from a YAML config file or dict.
|
|
@@ -365,6 +394,8 @@ class URRobot:
|
|
|
365
394
|
the ``gripper`` key from config.
|
|
366
395
|
default_vel: Default linear velocity (m/s).
|
|
367
396
|
default_acc: Default linear acceleration (m/s²).
|
|
397
|
+
ik_reference: IK reference posture name. Overrides the
|
|
398
|
+
``ik_reference`` key from config.
|
|
368
399
|
gripper_kwargs: Overrides for gripper preset values
|
|
369
400
|
(e.g. ``max_mm``, ``force``, ``speed``, ``pin``,
|
|
370
401
|
``mass``, ``center_of_gravity``, ``tcp_offset``).
|
|
@@ -375,6 +406,7 @@ class URRobot:
|
|
|
375
406
|
robot_ip: 192.168.1.50
|
|
376
407
|
points_path: points.db
|
|
377
408
|
gripper: hand-e
|
|
409
|
+
ik_reference: home # prevent elbow flipping
|
|
378
410
|
gripper_config:
|
|
379
411
|
mass: 1.5
|
|
380
412
|
force: 50
|
|
@@ -483,12 +515,24 @@ class URRobot:
|
|
|
483
515
|
if value is not None:
|
|
484
516
|
gripper_kwargs[key] = value
|
|
485
517
|
|
|
518
|
+
# Resolve ik_reference: explicit kwarg > config > None
|
|
519
|
+
resolved_ik: str | list[float] | None = None
|
|
520
|
+
if ik_reference is not None:
|
|
521
|
+
resolved_ik = ik_reference
|
|
522
|
+
else:
|
|
523
|
+
cfg_ik = cfg.get("ik_reference")
|
|
524
|
+
if isinstance(cfg_ik, str):
|
|
525
|
+
resolved_ik = cfg_ik
|
|
526
|
+
elif isinstance(cfg_ik, list) and len(cfg_ik) == 6:
|
|
527
|
+
resolved_ik = cfg_ik
|
|
528
|
+
|
|
486
529
|
return cls(
|
|
487
530
|
ip=resolved_ip,
|
|
488
531
|
points=resolved_points,
|
|
489
532
|
gripper=resolved_gripper,
|
|
490
533
|
default_vel=default_vel if default_vel is not None else cfg.get("default_vel", 0.5), # type: ignore
|
|
491
534
|
default_acc=default_acc if default_acc is not None else cfg.get("default_acc", 0.3), # type: ignore
|
|
535
|
+
ik_reference=resolved_ik,
|
|
492
536
|
**gripper_kwargs,
|
|
493
537
|
)
|
|
494
538
|
|
|
@@ -1009,6 +1053,7 @@ class URRobot:
|
|
|
1009
1053
|
offset_rx: float = 0.0,
|
|
1010
1054
|
offset_ry: float = 0.0,
|
|
1011
1055
|
offset_rz: float = 0.0,
|
|
1056
|
+
ik_reference: str | list[float] | None | Literal["__global__"] = "__global__",
|
|
1012
1057
|
) -> None:
|
|
1013
1058
|
"""Move to a saved point or raw pose.
|
|
1014
1059
|
|
|
@@ -1016,7 +1061,9 @@ class URRobot:
|
|
|
1016
1061
|
target: A saved point name (str) or a raw TCP pose
|
|
1017
1062
|
[x, y, z, rx, ry, rz].
|
|
1018
1063
|
linear: If True (default), use Cartesian linear move (moveL).
|
|
1019
|
-
If False, use joint-space move (moveJ).
|
|
1064
|
+
If False, use joint-space move (moveJ). When an IK
|
|
1065
|
+
reference is active, the pose is resolved to joints
|
|
1066
|
+
and a joint-space move is used regardless of this flag.
|
|
1020
1067
|
offset: Optional offset [dx, dy, dz, drx, dry, drz]
|
|
1021
1068
|
applied to the target pose before moving. Mutually
|
|
1022
1069
|
exclusive with individual offset_* parameters.
|
|
@@ -1032,6 +1079,15 @@ class URRobot:
|
|
|
1032
1079
|
offset_rx: Roll offset in radians (default 0.0).
|
|
1033
1080
|
offset_ry: Pitch offset in radians (default 0.0).
|
|
1034
1081
|
offset_rz: Yaw offset in radians (default 0.0).
|
|
1082
|
+
ik_reference: Per-call IK reference override. One of:
|
|
1083
|
+
|
|
1084
|
+
- ``"__global__"`` (default): Use the robot's global
|
|
1085
|
+
``ik_reference`` setting.
|
|
1086
|
+
- ``None``: Explicitly disable IK reference for this
|
|
1087
|
+
move (use controller's default behavior).
|
|
1088
|
+
- ``"current"``: Use current joint positions.
|
|
1089
|
+
- A point name (str): Look up and resolve to joints.
|
|
1090
|
+
- A 6-element list: Use directly as joint angles.
|
|
1035
1091
|
|
|
1036
1092
|
Raises:
|
|
1037
1093
|
MotionError: If the move fails or IK has no solution.
|
|
@@ -1044,6 +1100,8 @@ class URRobot:
|
|
|
1044
1100
|
>>> robot.move_to("pick", offset_x=0.01, offset_z=-0.02)
|
|
1045
1101
|
>>> robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full
|
|
1046
1102
|
>>> robot.move_to([0.5, 0, 0.3, 0, 0, 0]) # raw pose
|
|
1103
|
+
>>> robot.move_to("pick", ik_reference="home") # per-call override
|
|
1104
|
+
>>> robot.move_to("pick", ik_reference=None) # disable for one move
|
|
1047
1105
|
"""
|
|
1048
1106
|
self._check_connection()
|
|
1049
1107
|
self._disable_freedrive_guard()
|
|
@@ -1078,13 +1136,32 @@ class URRobot:
|
|
|
1078
1136
|
vel = vel if vel is not None else self._default_vel
|
|
1079
1137
|
acc = acc if acc is not None else self._default_acc
|
|
1080
1138
|
|
|
1139
|
+
# Resolve effective ik_reference: per-call > global
|
|
1140
|
+
effective_ik = (
|
|
1141
|
+
self._ik_reference if ik_reference == "__global__" else ik_reference
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1081
1144
|
try:
|
|
1082
|
-
if
|
|
1145
|
+
if effective_ik is not None:
|
|
1146
|
+
# IK reference active: resolve pose to joints with qnear,
|
|
1147
|
+
# then moveJ (prevents elbow/wrist flipping)
|
|
1148
|
+
qnear = self._resolve_ik_reference(effective_ik)
|
|
1149
|
+
joints = self.inverse_kinematics(pose, seed=qnear)
|
|
1150
|
+
self._move_target_joints = list(joints)
|
|
1151
|
+
self._move_target_pose = list(pose)
|
|
1152
|
+
self._motion.movej(joints, vel=vel, acc=acc, asynchronous=asynchronous)
|
|
1153
|
+
elif linear:
|
|
1154
|
+
# No IK reference, linear mode: standard moveL
|
|
1083
1155
|
if not self._rtde_c.getInverseKinematicsHasSolution(pose):
|
|
1084
1156
|
raise MotionError(f"Pose unreachable: {pose}")
|
|
1157
|
+
self._move_target_pose = list(pose)
|
|
1158
|
+
self._move_target_joints = None
|
|
1085
1159
|
self._motion.movel(pose, vel=vel, acc=acc, asynchronous=asynchronous)
|
|
1086
1160
|
else:
|
|
1161
|
+
# No IK reference, joint mode: resolve IK without qnear
|
|
1087
1162
|
joints = self.inverse_kinematics(pose)
|
|
1163
|
+
self._move_target_joints = list(joints)
|
|
1164
|
+
self._move_target_pose = list(pose)
|
|
1088
1165
|
self._motion.movej(joints, vel=vel, acc=acc, asynchronous=asynchronous)
|
|
1089
1166
|
except MotionError:
|
|
1090
1167
|
raise
|
|
@@ -1094,13 +1171,29 @@ class URRobot:
|
|
|
1094
1171
|
)
|
|
1095
1172
|
raise MotionError(f"Move to {target_label} failed: {e}")
|
|
1096
1173
|
|
|
1097
|
-
def is_moving(
|
|
1098
|
-
|
|
1174
|
+
def is_moving(
|
|
1175
|
+
self,
|
|
1176
|
+
*,
|
|
1177
|
+
position_tolerance: float = 0.002,
|
|
1178
|
+
orientation_tolerance: float = 0.035,
|
|
1179
|
+
joint_tolerance: float = 0.01,
|
|
1180
|
+
) -> bool:
|
|
1181
|
+
"""Check if the robot has arrived at the last move target.
|
|
1182
|
+
|
|
1183
|
+
Compares current TCP pose (or joint angles for joint moves)
|
|
1184
|
+
to the target stored by ``move_to()``. Returns False when
|
|
1185
|
+
within tolerance.
|
|
1099
1186
|
|
|
1100
|
-
|
|
1187
|
+
Args:
|
|
1188
|
+
position_tolerance: Max distance in meters to consider
|
|
1189
|
+
"arrived" (default 2 mm).
|
|
1190
|
+
orientation_tolerance: Max rotation error in radians to
|
|
1191
|
+
consider "arrived" (default ~2 degrees).
|
|
1192
|
+
joint_tolerance: Max per-joint error in radians for joint
|
|
1193
|
+
moves (default ~0.6 degrees).
|
|
1101
1194
|
|
|
1102
1195
|
Returns:
|
|
1103
|
-
True if moving, False if
|
|
1196
|
+
True if still moving toward target, False if arrived.
|
|
1104
1197
|
|
|
1105
1198
|
Example:
|
|
1106
1199
|
>>> robot.move_to("home", asynchronous=True)
|
|
@@ -1109,15 +1202,38 @@ class URRobot:
|
|
|
1109
1202
|
>>> print("Done!")
|
|
1110
1203
|
"""
|
|
1111
1204
|
try:
|
|
1112
|
-
|
|
1113
|
-
|
|
1205
|
+
# Joint-based: compare current joints to target
|
|
1206
|
+
if self._move_target_joints is not None:
|
|
1207
|
+
current = self._rtde_r.getActualQ()
|
|
1208
|
+
target = self._move_target_joints
|
|
1209
|
+
for c, t in zip(current, target):
|
|
1210
|
+
if abs(c - t) > joint_tolerance:
|
|
1211
|
+
return True
|
|
1212
|
+
self._move_target_joints = None
|
|
1213
|
+
self._move_target_pose = None
|
|
1214
|
+
return False
|
|
1114
1215
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1216
|
+
# Pose-based: compare current TCP pose to target
|
|
1217
|
+
if self._move_target_pose is not None:
|
|
1218
|
+
current = self._rtde_r.getActualTCPPose()
|
|
1219
|
+
target = self._move_target_pose
|
|
1220
|
+
dx = current[0] - target[0]
|
|
1221
|
+
dy = current[1] - target[1]
|
|
1222
|
+
dz = current[2] - target[2]
|
|
1223
|
+
dist = (dx * dx + dy * dy + dz * dz) ** 0.5
|
|
1224
|
+
|
|
1225
|
+
d_rx = abs(current[3] - target[3])
|
|
1226
|
+
d_ry = abs(current[4] - target[4])
|
|
1227
|
+
d_rz = abs(current[5] - target[5])
|
|
1228
|
+
orient_err = max(d_rx, d_ry, d_rz)
|
|
1229
|
+
|
|
1230
|
+
if dist <= position_tolerance and orient_err <= orientation_tolerance:
|
|
1231
|
+
self._move_target_pose = None
|
|
1232
|
+
return False
|
|
1233
|
+
return True
|
|
1119
1234
|
|
|
1120
|
-
|
|
1235
|
+
# No target set — assume not moving
|
|
1236
|
+
return False
|
|
1121
1237
|
except Exception:
|
|
1122
1238
|
return False
|
|
1123
1239
|
|
|
@@ -1136,6 +1252,10 @@ class URRobot:
|
|
|
1136
1252
|
self._rtde_c.stopL(5.0, True)
|
|
1137
1253
|
except Exception:
|
|
1138
1254
|
pass
|
|
1255
|
+
|
|
1256
|
+
# Clear move targets so is_moving() falls back to velocity check
|
|
1257
|
+
self._move_target_pose = None
|
|
1258
|
+
self._move_target_joints = None
|
|
1139
1259
|
try:
|
|
1140
1260
|
self._rtde_c.stopJ(5.0, True)
|
|
1141
1261
|
except Exception:
|
|
@@ -1156,6 +1276,7 @@ class URRobot:
|
|
|
1156
1276
|
delta_rx: float = 0.0,
|
|
1157
1277
|
delta_ry: float = 0.0,
|
|
1158
1278
|
delta_rz: float = 0.0,
|
|
1279
|
+
ik_reference: str | list[float] | None | Literal["__global__"] = "__global__",
|
|
1159
1280
|
) -> None:
|
|
1160
1281
|
"""Relative Cartesian move from the current position.
|
|
1161
1282
|
|
|
@@ -1166,7 +1287,9 @@ class URRobot:
|
|
|
1166
1287
|
delta: [dx, dy, dz, drx, dry, drz] in meters/radians.
|
|
1167
1288
|
Mutually exclusive with individual delta_* parameters.
|
|
1168
1289
|
linear: If True (default), use Cartesian linear move.
|
|
1169
|
-
If False, solve IK and use joint-space move.
|
|
1290
|
+
If False, solve IK and use joint-space move. When an IK
|
|
1291
|
+
reference is active, the pose is resolved to joints
|
|
1292
|
+
and a joint-space move is used regardless of this flag.
|
|
1170
1293
|
frame: Coordinate frame for the delta. Falls back to the
|
|
1171
1294
|
current ``move_frame`` property (BASE or TOOL).
|
|
1172
1295
|
vel: Velocity override. Falls back to default_vel.
|
|
@@ -1179,6 +1302,15 @@ class URRobot:
|
|
|
1179
1302
|
delta_rx: Roll delta in radians (default 0.0).
|
|
1180
1303
|
delta_ry: Pitch delta in radians (default 0.0).
|
|
1181
1304
|
delta_rz: Yaw delta in radians (default 0.0).
|
|
1305
|
+
ik_reference: Per-call IK reference override. One of:
|
|
1306
|
+
|
|
1307
|
+
- ``"__global__"`` (default): Use the robot's global
|
|
1308
|
+
``ik_reference`` setting.
|
|
1309
|
+
- ``None``: Explicitly disable IK reference for this
|
|
1310
|
+
move (use controller's default behavior).
|
|
1311
|
+
- ``"current"``: Use current joint positions.
|
|
1312
|
+
- A point name (str): Look up and resolve to joints.
|
|
1313
|
+
- A 6-element list: Use directly as joint angles.
|
|
1182
1314
|
|
|
1183
1315
|
Raises:
|
|
1184
1316
|
MotionError: If the move fails.
|
|
@@ -1221,20 +1353,91 @@ class URRobot:
|
|
|
1221
1353
|
acc = acc if acc is not None else self._default_acc
|
|
1222
1354
|
effective_frame = frame or self._move_frame
|
|
1223
1355
|
|
|
1356
|
+
# Resolve effective ik_reference: per-call > global
|
|
1357
|
+
effective_ik = (
|
|
1358
|
+
self._ik_reference if ik_reference == "__global__" else ik_reference
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1224
1361
|
try:
|
|
1225
1362
|
current = list(self._rtde_r.getActualTCPPose())
|
|
1226
1363
|
target = transform_pose_delta(current, final_delta, effective_frame)
|
|
1227
1364
|
|
|
1228
|
-
if
|
|
1365
|
+
if effective_ik is not None:
|
|
1366
|
+
# IK reference active: resolve pose to joints with qnear
|
|
1367
|
+
qnear = self._resolve_ik_reference(effective_ik)
|
|
1368
|
+
joints = self.inverse_kinematics(target, seed=qnear)
|
|
1369
|
+
self._move_target_joints = list(joints)
|
|
1370
|
+
self._move_target_pose = list(target)
|
|
1371
|
+
self._motion.movej(joints, vel=vel, acc=acc, asynchronous=asynchronous)
|
|
1372
|
+
elif linear:
|
|
1373
|
+
# No IK reference, linear mode: standard moveL
|
|
1374
|
+
self._move_target_pose = list(target)
|
|
1375
|
+
self._move_target_joints = None
|
|
1229
1376
|
self._motion.movel(target, vel=vel, acc=acc, asynchronous=asynchronous)
|
|
1230
1377
|
else:
|
|
1378
|
+
# No IK reference, joint mode: resolve IK without qnear
|
|
1231
1379
|
joints = self.inverse_kinematics(target)
|
|
1380
|
+
self._move_target_joints = list(joints)
|
|
1381
|
+
self._move_target_pose = list(target)
|
|
1232
1382
|
self._motion.movej(joints, vel=vel, acc=acc, asynchronous=asynchronous)
|
|
1233
1383
|
except MotionError:
|
|
1234
1384
|
raise
|
|
1235
1385
|
except Exception as e:
|
|
1236
1386
|
raise MotionError(f"Relative move failed: {e}")
|
|
1237
1387
|
|
|
1388
|
+
def _resolve_ik_reference(
|
|
1389
|
+
self,
|
|
1390
|
+
ik_reference: str | list[float],
|
|
1391
|
+
) -> list[float]:
|
|
1392
|
+
"""Resolve an ik_reference specifier to joint angles.
|
|
1393
|
+
|
|
1394
|
+
Args:
|
|
1395
|
+
ik_reference: One of:
|
|
1396
|
+
- ``"current"`` — use the robot's current joint positions.
|
|
1397
|
+
- A point name (str) — look up the point, resolve its pose
|
|
1398
|
+
to joints via IK. If the point doesn't exist, fall back
|
|
1399
|
+
to current joint positions with a warning.
|
|
1400
|
+
- A 6-element list — use directly as joint angles.
|
|
1401
|
+
|
|
1402
|
+
Returns:
|
|
1403
|
+
6 joint angles in radians.
|
|
1404
|
+
"""
|
|
1405
|
+
if ik_reference == "current":
|
|
1406
|
+
return list(self._rtde_r.getActualQ())
|
|
1407
|
+
|
|
1408
|
+
if isinstance(ik_reference, list) and len(ik_reference) == 6:
|
|
1409
|
+
return list(ik_reference)
|
|
1410
|
+
|
|
1411
|
+
if isinstance(ik_reference, str):
|
|
1412
|
+
# Try to look up as a named point
|
|
1413
|
+
if self._points is None:
|
|
1414
|
+
logger.warning(
|
|
1415
|
+
"ik_reference='%s': points DB not configured, "
|
|
1416
|
+
"falling back to current joint positions.",
|
|
1417
|
+
ik_reference,
|
|
1418
|
+
)
|
|
1419
|
+
return list(self._rtde_r.getActualQ())
|
|
1420
|
+
|
|
1421
|
+
point = self._points.get(ik_reference)
|
|
1422
|
+
if point is None:
|
|
1423
|
+
logger.warning(
|
|
1424
|
+
"ik_reference='%s': point not found, "
|
|
1425
|
+
"falling back to current joint positions. "
|
|
1426
|
+
"Available: %s",
|
|
1427
|
+
ik_reference,
|
|
1428
|
+
self._points.list(),
|
|
1429
|
+
)
|
|
1430
|
+
return list(self._rtde_r.getActualQ())
|
|
1431
|
+
|
|
1432
|
+
# Resolve the point's pose to joints (no qnear — just need
|
|
1433
|
+
# a representative joint config for this posture)
|
|
1434
|
+
return self.inverse_kinematics(point.pose)
|
|
1435
|
+
|
|
1436
|
+
raise MotionError(
|
|
1437
|
+
f"ik_reference must be a point name, 'current', or 6 joint "
|
|
1438
|
+
f"angles, got {type(ik_reference).__name__}."
|
|
1439
|
+
)
|
|
1440
|
+
|
|
1238
1441
|
def move_sequence(
|
|
1239
1442
|
self,
|
|
1240
1443
|
targets: list[str | list[float]],
|
|
@@ -1244,29 +1447,79 @@ class URRobot:
|
|
|
1244
1447
|
vel: float | None = None,
|
|
1245
1448
|
acc: float | None = None,
|
|
1246
1449
|
asynchronous: bool = False,
|
|
1450
|
+
ik_reference: str | list[float] | None | Literal["__global__"] = "__global__",
|
|
1247
1451
|
) -> None:
|
|
1248
1452
|
"""Move through a sequence of points.
|
|
1249
1453
|
|
|
1250
|
-
Executes each target in order
|
|
1251
|
-
|
|
1454
|
+
Executes each target in order. When ``ik_reference`` is provided,
|
|
1455
|
+
all poses are resolved to joint targets using chained inverse
|
|
1456
|
+
kinematics (each pose resolves relative to the previous one),
|
|
1457
|
+
then executed as a single blended joint-space path via
|
|
1458
|
+
``moveJ(path)``. This prevents the robot from unexpectedly
|
|
1459
|
+
flipping its elbow or wrist between waypoints.
|
|
1460
|
+
|
|
1461
|
+
Without ``ik_reference``, falls back to individual ``moveL``/
|
|
1462
|
+
``moveJ_IK`` calls (legacy behavior, no blending).
|
|
1252
1463
|
|
|
1253
1464
|
Args:
|
|
1254
1465
|
targets: List of saved point names or raw poses
|
|
1255
1466
|
[x, y, z, rx, ry, rz].
|
|
1256
|
-
linear:
|
|
1257
|
-
If
|
|
1258
|
-
|
|
1467
|
+
linear: Used only when ``ik_reference`` is ``None``.
|
|
1468
|
+
If True (default), use Cartesian linear moves (moveL).
|
|
1469
|
+
If False, use joint-space moves (moveJ_IK).
|
|
1470
|
+
Ignored when ``ik_reference`` is set (always uses
|
|
1471
|
+
joint-space path for IK stability).
|
|
1472
|
+
blend_radius: Blend radius in meters. Used only when
|
|
1473
|
+
``ik_reference`` is set — applied between consecutive
|
|
1474
|
+
waypoints in the joint path. Default 0.0 (stop at each
|
|
1475
|
+
waypoint). When set, the robot rounds corners instead
|
|
1476
|
+
of stopping at intermediate waypoints.
|
|
1259
1477
|
vel: Velocity override. Falls back to default_vel.
|
|
1260
1478
|
acc: Acceleration override. Falls back to default_acc.
|
|
1261
|
-
asynchronous:
|
|
1479
|
+
asynchronous: If True, move runs in background. Used only
|
|
1480
|
+
when ``ik_reference`` is set (path-based moves support
|
|
1481
|
+
async). Ignored in legacy mode.
|
|
1482
|
+
ik_reference: Reference posture for inverse kinematics
|
|
1483
|
+
resolution. One of:
|
|
1484
|
+
|
|
1485
|
+
- ``"__global__"`` (default): Use the robot's global
|
|
1486
|
+
``ik_reference`` setting. If the global setting is
|
|
1487
|
+
``None``, falls back to legacy behavior.
|
|
1488
|
+
- ``None``: Explicitly disable IK reference for this
|
|
1489
|
+
sequence (use controller's default behavior).
|
|
1490
|
+
- ``"current"``: Use the robot's current joint
|
|
1491
|
+
positions as the starting reference.
|
|
1492
|
+
- A point name (str): Look up the saved point,
|
|
1493
|
+
resolve its pose to joints, and use as the
|
|
1494
|
+
starting reference. If the point doesn't exist,
|
|
1495
|
+
falls back to current joint positions with a
|
|
1496
|
+
warning.
|
|
1497
|
+
- A 6-element list: Use directly as joint angles.
|
|
1498
|
+
|
|
1499
|
+
The reference is **chained** through the sequence:
|
|
1500
|
+
the first pose resolves relative to the reference,
|
|
1501
|
+
the second relative to the first's resolved joints,
|
|
1502
|
+
and so on. This keeps the arm configuration
|
|
1503
|
+
(elbow up/down, wrist orientation) consistent
|
|
1504
|
+
throughout the entire sequence.
|
|
1262
1505
|
|
|
1263
1506
|
Raises:
|
|
1264
|
-
MotionError: If the sequence fails
|
|
1265
|
-
|
|
1507
|
+
MotionError: If the sequence fails, fewer than 2 targets,
|
|
1508
|
+
or IK resolution fails for a pose.
|
|
1509
|
+
PointError: If a named target point is not found.
|
|
1266
1510
|
|
|
1267
1511
|
Example:
|
|
1268
|
-
>>> #
|
|
1512
|
+
>>> # Legacy: individual moveL calls, no IK control
|
|
1269
1513
|
>>> robot.move_sequence(["a", "b", "c"])
|
|
1514
|
+
|
|
1515
|
+
>>> # IK-stable: resolve to joints, blend between waypoints
|
|
1516
|
+
>>> robot.move_sequence(["a", "b", "c"],
|
|
1517
|
+
... ik_reference="home",
|
|
1518
|
+
... blend_radius=0.02)
|
|
1519
|
+
|
|
1520
|
+
>>> # Start from current posture
|
|
1521
|
+
>>> robot.move_sequence(["a", "b", "c"],
|
|
1522
|
+
... ik_reference="current")
|
|
1270
1523
|
"""
|
|
1271
1524
|
self._check_connection()
|
|
1272
1525
|
self._disable_freedrive_guard()
|
|
@@ -1279,23 +1532,67 @@ class URRobot:
|
|
|
1279
1532
|
v = vel if vel is not None else self._default_vel
|
|
1280
1533
|
a = acc if acc is not None else self._default_acc
|
|
1281
1534
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1535
|
+
# Resolve effective ik_reference: per-call > global
|
|
1536
|
+
effective_ik = (
|
|
1537
|
+
self._ik_reference if ik_reference == "__global__" else ik_reference
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
if effective_ik is not None:
|
|
1541
|
+
# --- IK-stable path mode: resolve all poses to joints,
|
|
1542
|
+
# build blended joint path, single moveJ(path) call. ---
|
|
1543
|
+
qnear = self._resolve_ik_reference(effective_ik)
|
|
1544
|
+
logger.info("move_sequence: ik_reference resolved to %s", qnear)
|
|
1545
|
+
|
|
1546
|
+
path: list[list[float]] = []
|
|
1547
|
+
for i, target in enumerate(targets):
|
|
1548
|
+
point = self._lookup_point(target)
|
|
1549
|
+
label = (
|
|
1550
|
+
f"'{target}'" if isinstance(target, str)
|
|
1551
|
+
else str(target[:3])
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
# Chain qnear: each pose resolves relative to previous
|
|
1555
|
+
joints = self.inverse_kinematics(point.pose, seed=qnear)
|
|
1556
|
+
logger.info(
|
|
1557
|
+
"move_sequence: %s (%d/%d) → joints %s",
|
|
1558
|
+
label, i + 1, len(targets),
|
|
1559
|
+
[f"{j:.3f}" for j in joints],
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
# 9-element path: [j0..j5, velocity, acceleration, blend]
|
|
1563
|
+
# Last waypoint gets blend=0 (come to rest)
|
|
1564
|
+
r = blend_radius if i < len(targets) - 1 else 0.0
|
|
1565
|
+
path.append([*joints, v, a, r])
|
|
1566
|
+
qnear = joints # chain to next
|
|
1567
|
+
|
|
1287
1568
|
logger.info(
|
|
1288
|
-
"move_sequence:
|
|
1569
|
+
"move_sequence: executing blended joint path (%d waypoints, "
|
|
1570
|
+
"blend=%.3f m)", len(path), blend_radius
|
|
1289
1571
|
)
|
|
1290
1572
|
try:
|
|
1291
|
-
|
|
1292
|
-
self._rtde_c.moveL(list(point.pose), v, a)
|
|
1293
|
-
else:
|
|
1294
|
-
self._rtde_c.moveJ_IK(
|
|
1295
|
-
list(point.pose), self._rtde_r.getActualQ(), v, a
|
|
1296
|
-
)
|
|
1573
|
+
self._rtde_c.moveJ(path, asynchronous=asynchronous)
|
|
1297
1574
|
except Exception as e:
|
|
1298
|
-
raise MotionError(f"move_sequence
|
|
1575
|
+
raise MotionError(f"move_sequence path execution failed: {e}")
|
|
1576
|
+
else:
|
|
1577
|
+
# --- Legacy mode: individual moveL/moveJ_IK calls. ---
|
|
1578
|
+
for i, target in enumerate(targets):
|
|
1579
|
+
point = self._lookup_point(target)
|
|
1580
|
+
label = (
|
|
1581
|
+
f"'{target}'" if isinstance(target, str)
|
|
1582
|
+
else str(target[:3])
|
|
1583
|
+
)
|
|
1584
|
+
logger.info(
|
|
1585
|
+
"move_sequence: %s (%d/%d)", label, i + 1, len(targets)
|
|
1586
|
+
)
|
|
1587
|
+
try:
|
|
1588
|
+
if linear:
|
|
1589
|
+
self._rtde_c.moveL(list(point.pose), v, a)
|
|
1590
|
+
else:
|
|
1591
|
+
self._rtde_c.moveJ_IK(list(point.pose), v, a)
|
|
1592
|
+
except Exception as e:
|
|
1593
|
+
raise MotionError(
|
|
1594
|
+
f"move_sequence failed at target {i}: {e}"
|
|
1595
|
+
)
|
|
1299
1596
|
|
|
1300
1597
|
def zero_ft_sensor(self) -> None:
|
|
1301
1598
|
"""Zero the robot's force/torque sensor.
|
|
@@ -1430,6 +1727,34 @@ class URRobot:
|
|
|
1430
1727
|
"""Current default acceleration (m/s²) for motion commands."""
|
|
1431
1728
|
return self._default_acc
|
|
1432
1729
|
|
|
1730
|
+
@property
|
|
1731
|
+
def ik_reference(self) -> str | list[float] | None:
|
|
1732
|
+
"""Current global IK reference posture.
|
|
1733
|
+
|
|
1734
|
+
Returns the reference used for inverse kinematics resolution
|
|
1735
|
+
in motion commands. One of:
|
|
1736
|
+
|
|
1737
|
+
- ``None``: No reference (controller picks IK solution).
|
|
1738
|
+
- ``"current"``: Use current joint positions.
|
|
1739
|
+
- A point name (str): Look up and resolve to joints.
|
|
1740
|
+
- A 6-element list: Use directly as joint angles.
|
|
1741
|
+
|
|
1742
|
+
Example:
|
|
1743
|
+
>>> robot.ik_reference = "home"
|
|
1744
|
+
>>> robot.move_to("pick") # uses home as IK reference
|
|
1745
|
+
>>> robot.ik_reference = None # disable
|
|
1746
|
+
"""
|
|
1747
|
+
return self._ik_reference
|
|
1748
|
+
|
|
1749
|
+
@ik_reference.setter
|
|
1750
|
+
def ik_reference(self, value: str | list[float] | None) -> None:
|
|
1751
|
+
"""Set the global IK reference posture.
|
|
1752
|
+
|
|
1753
|
+
Args:
|
|
1754
|
+
value: Reference posture (see getter docstring).
|
|
1755
|
+
"""
|
|
1756
|
+
self._ik_reference = value
|
|
1757
|
+
|
|
1433
1758
|
def set_speed(self, vel: float) -> None:
|
|
1434
1759
|
"""Change the default velocity for subsequent motions.
|
|
1435
1760
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.20
|
|
4
4
|
Summary: Universal Robots e-Series control toolkit built on ur_rtde
|
|
5
5
|
Author: URKit Contributors
|
|
6
6
|
License: MIT
|
|
@@ -424,6 +424,43 @@ robot.move_relative(delta_z=0.05) # 5cm along tool Z
|
|
|
424
424
|
- **BASE** (default): delta relative to robot base
|
|
425
425
|
- **TOOL**: delta relative to TCP orientation
|
|
426
426
|
|
|
427
|
+
#### IK Reference (recommended)
|
|
428
|
+
|
|
429
|
+
**Problem:** When the robot has multiple valid joint configurations to reach the same TCP pose (e.g., elbow up vs. elbow down), it can unexpectedly flip its posture between moves. This is called an **IK ambiguity** and it causes "weird" movements where the robot takes a strange path or flips its wrist.
|
|
430
|
+
|
|
431
|
+
**Solution:** Set an **IK reference posture** — a saved point that defines your preferred arm configuration. The robot then stays close to that posture for all moves.
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
# 1. Put the robot in your preferred posture (e.g., "home")
|
|
435
|
+
# 2. Save it: robot.save_point("home")
|
|
436
|
+
# 3. Set as IK reference:
|
|
437
|
+
robot.ik_reference = "home"
|
|
438
|
+
|
|
439
|
+
# Now all moves stay close to that posture — no elbow flipping
|
|
440
|
+
robot.move_to("pick")
|
|
441
|
+
robot.move_to("place")
|
|
442
|
+
robot.move_relative(delta_z=-0.05)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**In config.yaml** (recommended for permanent setup):
|
|
446
|
+
|
|
447
|
+
```yaml
|
|
448
|
+
robot_ip: 192.168.1.50
|
|
449
|
+
ik_reference: home # prevents weird elbow/wrist flips
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**How it works:** The robot's inverse kinematics solver uses the reference posture as a bias (`qnear`). The TCP still reaches the exact same pose, but the arm configuration (elbow up/down, wrist orientation) stays consistent with your reference.
|
|
453
|
+
|
|
454
|
+
**Per-move override:**
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
robot.ik_reference = "home" # global default
|
|
458
|
+
robot.move_to("weird_pose", ik_reference=None) # one move without it
|
|
459
|
+
robot.move_to("back", ik_reference="current") # use current joints
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**When to use it:** Almost always. If you've ever seen the robot move in a way that looked "wrong" or flipped its elbow unexpectedly, this is what fixes it. Set it once in config.yaml and forget about it.
|
|
463
|
+
|
|
427
464
|
#### Points are tool-agnostic
|
|
428
465
|
|
|
429
466
|
Points are stored in the active TCP frame, so they work with any tool. If you swap grippers and set the correct TCP offset, your saved points remain valid.
|
|
@@ -589,6 +626,7 @@ URKit searches for `config.yaml` in this order:
|
|
|
589
626
|
| `robot_ip` | Robot IP address | `192.168.1.50` |
|
|
590
627
|
| `points_path` | Path to SQLite points database | `points.db` |
|
|
591
628
|
| `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `digital` |
|
|
629
|
+
| `ik_reference` | IK reference posture (prevents elbow flipping) | `home` |
|
|
592
630
|
| `default_vel` | Default linear velocity (m/s) | `0.5` |
|
|
593
631
|
| `default_acc` | Default linear acceleration (m/s²) | `0.3` |
|
|
594
632
|
| `expert_mode` | Disable safety speed clamping | `false` |
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Unit tests for move_sequence with ik_reference and blending.
|
|
2
|
+
|
|
3
|
+
Tests the IK resolution, path building, and blending logic
|
|
4
|
+
without requiring a real robot.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from urkit.exceptions import MotionError, PointError
|
|
15
|
+
from urkit.points import Point, Points
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ------------------------------------------------------------------
|
|
19
|
+
# Fixtures
|
|
20
|
+
# ------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def mock_rtde_c():
|
|
25
|
+
"""Mock RTDEControlInterface."""
|
|
26
|
+
mock = MagicMock()
|
|
27
|
+
mock.isConnected.return_value = True
|
|
28
|
+
mock.getInverseKinematicsHasSolution.return_value = True
|
|
29
|
+
mock.getInverseKinematics.return_value = [0.0, -0.5, 0.5, -1.5, 1.5, 0.0]
|
|
30
|
+
return mock
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def mock_rtde_r():
|
|
35
|
+
"""Mock RTDEReceiveInterface."""
|
|
36
|
+
mock = MagicMock()
|
|
37
|
+
mock.getActualQ.return_value = [0.0, -1.0, 1.0, -1.57, 1.57, 0.0]
|
|
38
|
+
mock.getActualTCPPose.return_value = [0.5, 0.0, 0.3, 0.0, 0.0, 0.0]
|
|
39
|
+
return mock
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def mock_points(tmp_path):
|
|
44
|
+
"""Create a temporary points database with test data."""
|
|
45
|
+
db_path = tmp_path / "test_points.db"
|
|
46
|
+
pts = Points(db_path)
|
|
47
|
+
pts.save(Point(name="home", pose=[0.5, 0.0, 0.3, 0.0, 0.0, 0.0]))
|
|
48
|
+
pts.save(Point(name="pick", pose=[0.3, 0.2, 0.1, 0.0, 0.0, 0.0]))
|
|
49
|
+
pts.save(Point(name="place", pose=[0.3, -0.2, 0.1, 0.0, 0.0, 0.0]))
|
|
50
|
+
return pts
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def robot(mock_rtde_c, mock_rtde_r, mock_points, tmp_path):
|
|
55
|
+
"""Create a URRobot with mocked RTDE interfaces."""
|
|
56
|
+
with patch("urkit.robot._validate_connection"), \
|
|
57
|
+
patch("urkit.robot._check_remote_mode"), \
|
|
58
|
+
patch("urkit.robot._connect_rtde") as mock_connect, \
|
|
59
|
+
patch("urkit.robot._connect_dashboard"), \
|
|
60
|
+
patch("urkit.robot._try_recover_safety"):
|
|
61
|
+
|
|
62
|
+
# Set up the connect mock to populate rtde interfaces
|
|
63
|
+
mock_connect.return_value = (mock_rtde_c, mock_rtde_r)
|
|
64
|
+
|
|
65
|
+
from urkit.robot import URRobot
|
|
66
|
+
robot = object.__new__(URRobot) # bypass __init__
|
|
67
|
+
robot._ip = "127.0.0.1"
|
|
68
|
+
robot._rtde_c = mock_rtde_c
|
|
69
|
+
robot._rtde_r = mock_rtde_r
|
|
70
|
+
robot._rtde_frequency = 500.0
|
|
71
|
+
robot._connection_lost = False
|
|
72
|
+
robot._default_vel = 0.5
|
|
73
|
+
robot._default_acc = 0.3
|
|
74
|
+
robot._points = mock_points
|
|
75
|
+
robot._move_frame = None
|
|
76
|
+
robot._move_target_joints = None
|
|
77
|
+
robot._ik_reference = None
|
|
78
|
+
|
|
79
|
+
# Mock the motion object
|
|
80
|
+
robot._motion = MagicMock()
|
|
81
|
+
|
|
82
|
+
# Mock _check_connection and _disable_freedrive_guard
|
|
83
|
+
robot._check_connection = MagicMock()
|
|
84
|
+
robot._disable_freedrive_guard = MagicMock()
|
|
85
|
+
|
|
86
|
+
# Mock inverse_kinematics to delegate to rtde_c
|
|
87
|
+
original_ik = robot.inverse_kinematics.__func__ if hasattr(robot.inverse_kinematics, '__func__') else None
|
|
88
|
+
|
|
89
|
+
def mock_ik(pose, seed=None):
|
|
90
|
+
qnear = seed if seed is not None else []
|
|
91
|
+
if not mock_rtde_c.getInverseKinematicsHasSolution(pose, qnear):
|
|
92
|
+
raise MotionError(f"No IK solution for pose {pose}")
|
|
93
|
+
return mock_rtde_c.getInverseKinematics(pose, qnear)
|
|
94
|
+
|
|
95
|
+
# Bind mock_ik to the robot instance
|
|
96
|
+
import types
|
|
97
|
+
robot.inverse_kinematics = types.MethodType(
|
|
98
|
+
lambda self, pose, seed=None: mock_ik(pose, seed), robot
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return robot
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
# Tests: _resolve_ik_reference
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestResolveIkReference:
|
|
110
|
+
"""Test the _resolve_ik_reference helper method."""
|
|
111
|
+
|
|
112
|
+
def test_current_returns_actual_joints(self, robot, mock_rtde_r):
|
|
113
|
+
result = robot._resolve_ik_reference("current")
|
|
114
|
+
assert result == list(mock_rtde_r.getActualQ())
|
|
115
|
+
|
|
116
|
+
def test_point_name_resolves_to_joints(self, robot, mock_rtde_c):
|
|
117
|
+
"""Named point should be looked up and resolved via IK."""
|
|
118
|
+
result = robot._resolve_ik_reference("home")
|
|
119
|
+
# Should call inverse_kinematics with the point's pose
|
|
120
|
+
assert len(result) == 6
|
|
121
|
+
# The mock returns the default IK result
|
|
122
|
+
assert result == [0.0, -0.5, 0.5, -1.5, 1.5, 0.0]
|
|
123
|
+
|
|
124
|
+
def test_missing_point_falls_back_to_current(self, robot, mock_rtde_r, caplog):
|
|
125
|
+
"""Non-existent point name should fall back to current joints."""
|
|
126
|
+
with caplog.at_level(logging.WARNING):
|
|
127
|
+
result = robot._resolve_ik_reference("nonexistent")
|
|
128
|
+
assert result == list(mock_rtde_r.getActualQ())
|
|
129
|
+
assert "point not found" in caplog.text or "falling back" in caplog.text
|
|
130
|
+
|
|
131
|
+
def test_joints_list_used_directly(self, robot):
|
|
132
|
+
"""Raw joint list should be returned as-is."""
|
|
133
|
+
joints = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
|
|
134
|
+
result = robot._resolve_ik_reference(joints)
|
|
135
|
+
assert result == joints
|
|
136
|
+
|
|
137
|
+
def test_no_points_db_falls_back(self, robot, mock_rtde_r, caplog):
|
|
138
|
+
"""When points DB is None, should fall back to current joints."""
|
|
139
|
+
robot._points = None
|
|
140
|
+
result = robot._resolve_ik_reference("home")
|
|
141
|
+
assert result == list(mock_rtde_r.getActualQ())
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
# Tests: move_sequence validation
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestMoveSequenceValidation:
|
|
150
|
+
"""Test move_sequence input validation."""
|
|
151
|
+
|
|
152
|
+
def test_requires_at_least_two_targets(self, robot):
|
|
153
|
+
with pytest.raises(MotionError, match="at least 2"):
|
|
154
|
+
robot.move_sequence(["single_point"])
|
|
155
|
+
|
|
156
|
+
def test_empty_targets_raises(self, robot):
|
|
157
|
+
with pytest.raises(MotionError, match="at least 2"):
|
|
158
|
+
robot.move_sequence([])
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# Tests: move_sequence legacy mode (no ik_reference)
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestMoveSequenceLegacy:
|
|
167
|
+
"""Test move_sequence without ik_reference (legacy behavior)."""
|
|
168
|
+
|
|
169
|
+
def test_linear_mode_calls_movel(self, robot, mock_rtde_c):
|
|
170
|
+
"""Without ik_reference and linear=True, should call moveL."""
|
|
171
|
+
robot.move_sequence(["home", "pick"])
|
|
172
|
+
# Should have called moveL twice (once per target)
|
|
173
|
+
assert mock_rtde_c.moveL.call_count == 2
|
|
174
|
+
|
|
175
|
+
def test_joints_mode_calls_movej_ik(self, robot, mock_rtde_c):
|
|
176
|
+
"""Without ik_reference and linear=False, should call moveJ_IK."""
|
|
177
|
+
robot.move_sequence(["home", "pick"], linear=False)
|
|
178
|
+
assert mock_rtde_c.moveJ_IK.call_count == 2
|
|
179
|
+
|
|
180
|
+
def test_raw_poses_work(self, robot, mock_rtde_c):
|
|
181
|
+
"""Raw pose lists should work as targets."""
|
|
182
|
+
poses = [
|
|
183
|
+
[0.5, 0.0, 0.3, 0.0, 0.0, 0.0],
|
|
184
|
+
[0.3, 0.2, 0.1, 0.0, 0.0, 0.0],
|
|
185
|
+
]
|
|
186
|
+
robot.move_sequence(poses)
|
|
187
|
+
assert mock_rtde_c.moveL.call_count == 2
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
# Tests: move_sequence with ik_reference
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestMoveSequenceWithIkReference:
|
|
196
|
+
"""Test move_sequence with ik_reference for IK stability."""
|
|
197
|
+
|
|
198
|
+
def test_ik_reference_resolves_path(self, robot, mock_rtde_c):
|
|
199
|
+
"""With ik_reference, should call moveJ(path) once."""
|
|
200
|
+
robot.move_sequence(["home", "pick", "place"], ik_reference="current")
|
|
201
|
+
# Should call moveJ once with a path (not individual moveL)
|
|
202
|
+
assert mock_rtde_c.moveJ.call_count == 1
|
|
203
|
+
# moveL should NOT be called
|
|
204
|
+
assert mock_rtde_c.moveL.call_count == 0
|
|
205
|
+
|
|
206
|
+
def test_path_format_is_nine_elements(self, robot, mock_rtde_c):
|
|
207
|
+
"""Each waypoint in the path should have 9 elements."""
|
|
208
|
+
robot.move_sequence(["home", "pick"], ik_reference="current")
|
|
209
|
+
call_args = mock_rtde_c.moveJ.call_args
|
|
210
|
+
path = call_args[0][0] # first positional arg
|
|
211
|
+
assert len(path) == 2 # two waypoints
|
|
212
|
+
for waypoint in path:
|
|
213
|
+
assert len(waypoint) == 9 # [j0..j5, vel, acc, blend]
|
|
214
|
+
|
|
215
|
+
def test_last_waypoint_has_zero_blend(self, robot, mock_rtde_c):
|
|
216
|
+
"""Last waypoint should have blend_radius=0 (come to rest)."""
|
|
217
|
+
robot.move_sequence(
|
|
218
|
+
["home", "pick", "place"],
|
|
219
|
+
ik_reference="current",
|
|
220
|
+
blend_radius=0.02,
|
|
221
|
+
)
|
|
222
|
+
path = mock_rtde_c.moveJ.call_args[0][0]
|
|
223
|
+
# Last waypoint blend should be 0
|
|
224
|
+
assert path[-1][8] == 0.0
|
|
225
|
+
# Intermediate waypoints should have the blend radius
|
|
226
|
+
assert path[0][8] == 0.02
|
|
227
|
+
|
|
228
|
+
def test_chained_ik_calls(self, robot, mock_rtde_c):
|
|
229
|
+
"""IK should be called once per target (chained)."""
|
|
230
|
+
# Reset the call count
|
|
231
|
+
mock_rtde_c.getInverseKinematics.reset_mock()
|
|
232
|
+
|
|
233
|
+
robot.move_sequence(["home", "pick", "place"], ik_reference="current")
|
|
234
|
+
|
|
235
|
+
# 3 targets = 3 IK calls (each chained from previous)
|
|
236
|
+
assert mock_rtde_c.getInverseKinematics.call_count == 3
|
|
237
|
+
|
|
238
|
+
def test_ik_reference_point_name(self, robot, mock_rtde_c):
|
|
239
|
+
"""ik_reference as a point name should resolve via IK."""
|
|
240
|
+
robot.move_sequence(["pick", "place"], ik_reference="home")
|
|
241
|
+
assert mock_rtde_c.moveJ.call_count == 1
|
|
242
|
+
|
|
243
|
+
def test_ik_reference_joints_list(self, robot, mock_rtde_c):
|
|
244
|
+
"""ik_reference as a raw joints list should work."""
|
|
245
|
+
ref_joints = [0.0, -1.0, 1.0, -1.57, 1.57, 0.0]
|
|
246
|
+
robot.move_sequence(["pick", "place"], ik_reference=ref_joints)
|
|
247
|
+
assert mock_rtde_c.moveJ.call_count == 1
|
|
248
|
+
|
|
249
|
+
def test_async_passed_through(self, robot, mock_rtde_c):
|
|
250
|
+
"""asynchronous parameter should be passed to moveJ(path)."""
|
|
251
|
+
robot.move_sequence(
|
|
252
|
+
["home", "pick"],
|
|
253
|
+
ik_reference="current",
|
|
254
|
+
asynchronous=True,
|
|
255
|
+
)
|
|
256
|
+
call_kwargs = mock_rtde_c.moveJ.call_args[1]
|
|
257
|
+
assert call_kwargs.get("asynchronous") is True
|
|
258
|
+
|
|
259
|
+
def test_vel_acc_in_path(self, robot, mock_rtde_c):
|
|
260
|
+
"""Velocity and acceleration should be in each path waypoint."""
|
|
261
|
+
robot.move_sequence(
|
|
262
|
+
["home", "pick"],
|
|
263
|
+
ik_reference="current",
|
|
264
|
+
vel=0.8,
|
|
265
|
+
acc=0.5,
|
|
266
|
+
)
|
|
267
|
+
path = mock_rtde_c.moveJ.call_args[0][0]
|
|
268
|
+
for waypoint in path:
|
|
269
|
+
assert waypoint[6] == 0.8 # velocity
|
|
270
|
+
assert waypoint[7] == 0.5 # acceleration
|
|
271
|
+
|
|
272
|
+
def test_defaults_used_when_no_vel_acc(self, robot, mock_rtde_c):
|
|
273
|
+
"""Default vel/acc should be used when not specified."""
|
|
274
|
+
robot._default_vel = 0.5
|
|
275
|
+
robot._default_acc = 0.3
|
|
276
|
+
robot.move_sequence(["home", "pick"], ik_reference="current")
|
|
277
|
+
path = mock_rtde_c.moveJ.call_args[0][0]
|
|
278
|
+
for waypoint in path:
|
|
279
|
+
assert waypoint[6] == 0.5
|
|
280
|
+
assert waypoint[7] == 0.3
|
|
281
|
+
|
|
282
|
+
def test_global_ik_reference_used_when_not_specified(self, robot, mock_rtde_c):
|
|
283
|
+
"""When ik_reference is not specified, should use global setting."""
|
|
284
|
+
robot._ik_reference = "home"
|
|
285
|
+
mock_rtde_c.getInverseKinematics.reset_mock()
|
|
286
|
+
robot.move_sequence(["pick", "place"]) # no ik_reference arg
|
|
287
|
+
# Should use global ik_reference and call moveJ(path)
|
|
288
|
+
assert mock_rtde_c.moveJ.call_count == 1
|
|
289
|
+
assert mock_rtde_c.getInverseKinematics.call_count >= 2
|
|
290
|
+
|
|
291
|
+
def test_per_call_overrides_global(self, robot, mock_rtde_c):
|
|
292
|
+
"""Per-call ik_reference=None should override global setting."""
|
|
293
|
+
robot._ik_reference = "home"
|
|
294
|
+
robot.move_sequence(["pick", "place"], ik_reference=None)
|
|
295
|
+
# Should use legacy mode (individual moveL)
|
|
296
|
+
assert mock_rtde_c.moveL.call_count == 2
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|