urkit 0.3.6__tar.gz → 0.3.8__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.
Files changed (39) hide show
  1. {urkit-0.3.6 → urkit-0.3.8}/PKG-INFO +20 -7
  2. {urkit-0.3.6 → urkit-0.3.8}/README.md +19 -6
  3. {urkit-0.3.6 → urkit-0.3.8}/pyproject.toml +1 -1
  4. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/__init__.py +1 -1
  5. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/cli/teach.py +1 -1
  6. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/motion.py +28 -4
  7. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/robot.py +137 -28
  8. {urkit-0.3.6 → urkit-0.3.8}/src/urkit.egg-info/PKG-INFO +20 -7
  9. {urkit-0.3.6 → urkit-0.3.8}/setup.cfg +0 -0
  10. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/__main__.py +0 -0
  11. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/cli/__init__.py +0 -0
  12. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/cli/colors.py +0 -0
  13. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/cli/connection_monitor.py +0 -0
  14. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/cli/points.py +0 -0
  15. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/config.py +0 -0
  16. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/connection.py +0 -0
  17. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/exceptions.py +0 -0
  18. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/geometry.py +0 -0
  19. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/gripper/__init__.py +0 -0
  20. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/gripper/base.py +0 -0
  21. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/gripper/digital.py +0 -0
  22. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/gripper/presets.py +0 -0
  23. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/gripper/robotiq.py +0 -0
  24. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/gripper/robotiq_preamble.py +0 -0
  25. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/io.py +0 -0
  26. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/points.py +0 -0
  27. {urkit-0.3.6 → urkit-0.3.8}/src/urkit/telemetry.py +0 -0
  28. {urkit-0.3.6 → urkit-0.3.8}/src/urkit.egg-info/SOURCES.txt +0 -0
  29. {urkit-0.3.6 → urkit-0.3.8}/src/urkit.egg-info/dependency_links.txt +0 -0
  30. {urkit-0.3.6 → urkit-0.3.8}/src/urkit.egg-info/entry_points.txt +0 -0
  31. {urkit-0.3.6 → urkit-0.3.8}/src/urkit.egg-info/requires.txt +0 -0
  32. {urkit-0.3.6 → urkit-0.3.8}/src/urkit.egg-info/top_level.txt +0 -0
  33. {urkit-0.3.6 → urkit-0.3.8}/tests/test_exceptions.py +0 -0
  34. {urkit-0.3.6 → urkit-0.3.8}/tests/test_geometry.py +0 -0
  35. {urkit-0.3.6 → urkit-0.3.8}/tests/test_gripper.py +0 -0
  36. {urkit-0.3.6 → urkit-0.3.8}/tests/test_gripper_factory.py +0 -0
  37. {urkit-0.3.6 → urkit-0.3.8}/tests/test_gripper_presets.py +0 -0
  38. {urkit-0.3.6 → urkit-0.3.8}/tests/test_points.py +0 -0
  39. {urkit-0.3.6 → urkit-0.3.8}/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.6
3
+ Version: 0.3.8
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -88,7 +88,7 @@ robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
88
88
  robot.gripper.activate()
89
89
 
90
90
  robot.move_to("home")
91
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0, 0])
91
+ robot.move_to("pick", offset_z=0.05) # 5cm above
92
92
  robot.gripper.close()
93
93
  robot.move_to("place")
94
94
  robot.gripper.open()
@@ -394,10 +394,12 @@ A pose is `[x, y, z, rx, ry, rz]`: position in meters and orientation as a **rot
394
394
 
395
395
  #### Offsets
396
396
 
397
- Offsets are 6-element lists `[dx, dy, dz, drx, dry, drz]`:
397
+ Offsets can use individual parameters or a full 6-element list:
398
398
 
399
399
  ```python
400
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0, 0]) # 5cm above pick
400
+ robot.move_to("pick", offset_z=0.05) # 5cm above
401
+ robot.move_to("pick", offset_x=0.01, offset_z=-0.02) # combined
402
+ robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
401
403
  ```
402
404
 
403
405
  #### Resolve a Pose
@@ -416,7 +418,7 @@ robot.move_to(pose) # move to the resolved pose later
416
418
  from urkit import MoveFrame
417
419
 
418
420
  robot.move_frame = MoveFrame.TOOL # default is BASE
419
- robot.move_relative([0, 0, 0.05, 0, 0, 0]) # 5cm along tool Z
421
+ robot.move_relative(delta_z=0.05) # 5cm along tool Z
420
422
  ```
421
423
 
422
424
  - **BASE** (default): delta relative to robot base
@@ -440,8 +442,9 @@ robot.import_points("backup.json")
440
442
  #### Relative Moves
441
443
 
442
444
  ```python
443
- robot.move_relative([0, 0.01, 0, 0, 0, 0]) # 1cm along Y
444
- robot.move_relative([0, 0, 0.05, 0, 0, 0], frame=MoveFrame.TOOL) # 5cm along tool Z
445
+ robot.move_relative(delta_y=0.01) # 1cm along Y
446
+ robot.move_relative(delta_z=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
447
+ robot.move_relative([0, 0.01, 0, 0, 0, 0])
445
448
  ```
446
449
 
447
450
  #### Sequences with Blending
@@ -454,7 +457,17 @@ robot.move_sequence(["a", "b", "c"], blend_radius=0.02)
454
457
  #### Contact Detection
455
458
 
456
459
  ```python
460
+ # Zeros FT sensor automatically, then moves until force exceeds threshold
457
461
  robot.move_until_contact([0, 0, -0.02, 0, 0, 0])
462
+
463
+ # Custom threshold (default: 5.0 N/Nm)
464
+ robot.move_until_contact([0, 0, -0.02, 0, 0, 0], threshold=10.0)
465
+
466
+ # Skip zeroing if you need absolute force values
467
+ robot.move_until_contact([0, 0, -0.02, 0, 0, 0], zero_first=False)
468
+
469
+ # Manual zero (e.g. before custom force-based logic)
470
+ robot.zero_ft_sensor()
458
471
  ```
459
472
 
460
473
  #### Velocity Control
@@ -62,7 +62,7 @@ robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
62
62
  robot.gripper.activate()
63
63
 
64
64
  robot.move_to("home")
65
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0, 0])
65
+ robot.move_to("pick", offset_z=0.05) # 5cm above
66
66
  robot.gripper.close()
67
67
  robot.move_to("place")
68
68
  robot.gripper.open()
@@ -368,10 +368,12 @@ A pose is `[x, y, z, rx, ry, rz]`: position in meters and orientation as a **rot
368
368
 
369
369
  #### Offsets
370
370
 
371
- Offsets are 6-element lists `[dx, dy, dz, drx, dry, drz]`:
371
+ Offsets can use individual parameters or a full 6-element list:
372
372
 
373
373
  ```python
374
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0, 0]) # 5cm above pick
374
+ robot.move_to("pick", offset_z=0.05) # 5cm above
375
+ robot.move_to("pick", offset_x=0.01, offset_z=-0.02) # combined
376
+ robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
375
377
  ```
376
378
 
377
379
  #### Resolve a Pose
@@ -390,7 +392,7 @@ robot.move_to(pose) # move to the resolved pose later
390
392
  from urkit import MoveFrame
391
393
 
392
394
  robot.move_frame = MoveFrame.TOOL # default is BASE
393
- robot.move_relative([0, 0, 0.05, 0, 0, 0]) # 5cm along tool Z
395
+ robot.move_relative(delta_z=0.05) # 5cm along tool Z
394
396
  ```
395
397
 
396
398
  - **BASE** (default): delta relative to robot base
@@ -414,8 +416,9 @@ robot.import_points("backup.json")
414
416
  #### Relative Moves
415
417
 
416
418
  ```python
417
- robot.move_relative([0, 0.01, 0, 0, 0, 0]) # 1cm along Y
418
- robot.move_relative([0, 0, 0.05, 0, 0, 0], frame=MoveFrame.TOOL) # 5cm along tool Z
419
+ robot.move_relative(delta_y=0.01) # 1cm along Y
420
+ robot.move_relative(delta_z=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
421
+ robot.move_relative([0, 0.01, 0, 0, 0, 0])
419
422
  ```
420
423
 
421
424
  #### Sequences with Blending
@@ -428,7 +431,17 @@ robot.move_sequence(["a", "b", "c"], blend_radius=0.02)
428
431
  #### Contact Detection
429
432
 
430
433
  ```python
434
+ # Zeros FT sensor automatically, then moves until force exceeds threshold
431
435
  robot.move_until_contact([0, 0, -0.02, 0, 0, 0])
436
+
437
+ # Custom threshold (default: 5.0 N/Nm)
438
+ robot.move_until_contact([0, 0, -0.02, 0, 0, 0], threshold=10.0)
439
+
440
+ # Skip zeroing if you need absolute force values
441
+ robot.move_until_contact([0, 0, -0.02, 0, 0, 0], zero_first=False)
442
+
443
+ # Manual zero (e.g. before custom force-based logic)
444
+ robot.zero_ft_sensor()
432
445
  ```
433
446
 
434
447
  #### Velocity Control
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "urkit"
7
- version = "0.3.6"
7
+ version = "0.3.8"
8
8
  description = "Universal Robots e-Series control toolkit built on ur_rtde"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -24,7 +24,7 @@ Quick start::
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
- __version__ = "0.3.6"
27
+ __version__ = "0.3.8"
28
28
 
29
29
  from urkit.config import load_config, resolve_config
30
30
  from urkit.exceptions import (
@@ -1441,7 +1441,7 @@ def teach_command(args) -> None:
1441
1441
  **gripper_kwargs,
1442
1442
  )
1443
1443
  print(" Connected.", flush=True)
1444
- if robot.activate_gripper():
1444
+ if robot._activate_gripper():
1445
1445
  print(" Gripper activated.", flush=True)
1446
1446
  except ConnectionError as e:
1447
1447
  print(f"Connection error: {e}")
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  import logging
11
11
  import os
12
12
  import sys
13
- import time as _time
13
+ import time
14
14
  from contextlib import contextmanager
15
15
  from enum import IntEnum
16
16
  from typing import Iterator
@@ -255,12 +255,28 @@ class Motion:
255
255
  f"Relative move failed: {e}"
256
256
  )
257
257
 
258
+ def zero_ft_sensor(self) -> None:
259
+ """Zero the robot's force/torque sensor.
260
+
261
+ Clears the baseline so that subsequent ``getActualTCPForce()``
262
+ readings reflect only forces applied after this call. Call before
263
+ ``move_until_contact()`` or any force-based operation.
264
+
265
+ Raises:
266
+ MotionError: If the command fails.
267
+ """
268
+ try:
269
+ self._rtde_c.zeroFtSensor()
270
+ except Exception as e:
271
+ raise MotionError(f"Failed to zero FT sensor: {e}")
272
+
258
273
  def move_until_contact(
259
274
  self,
260
275
  speed_vector: list[float],
261
276
  *,
262
277
  threshold: float = 5.0,
263
278
  acceleration: float = 0.1,
279
+ zero_first: bool = True,
264
280
  ) -> None:
265
281
  """Move until contact is detected via TCP force sensing.
266
282
 
@@ -280,12 +296,15 @@ class Motion:
280
296
  Contact fires when any of the 6 wrench components changes
281
297
  by more than this value from the baseline reading.
282
298
  acceleration: Acceleration limit passed to ``speedL()`` in m/s².
299
+ zero_first: If True (default), zero the FT sensor before reading
300
+ the baseline. Set to False if you need absolute force values
301
+ rather than delta from zero.
283
302
 
284
303
  Raises:
285
304
  MotionError: If the command fails or the vector is invalid.
286
305
 
287
306
  Example:
288
- >>> # Move straight down until contact
307
+ >>> # Move straight down until contact (zeros FT sensor first)
289
308
  >>> motion.move_until_contact([0, 0, -0.02, 0, 0, 0])
290
309
  >>> # Move down while rotating, higher threshold
291
310
  >>> motion.move_until_contact([0, 0, -0.02, 0, 0.1, 0], threshold=10.0)
@@ -303,6 +322,11 @@ class Motion:
303
322
  speed_vector, threshold,
304
323
  )
305
324
 
325
+ # Zero FT sensor to clear gravity bias before reading baseline
326
+ if zero_first:
327
+ self.zero_ft_sensor()
328
+ time.sleep(0.05) # let sensor settle after zero
329
+
306
330
  # Baseline force reading before the loop
307
331
  baseline = list(self._rtde_r.getActualTCPForce())
308
332
 
@@ -387,9 +411,9 @@ class Motion:
387
411
  raise MotionError(f"Duration must be > 0, got {duration}.")
388
412
 
389
413
  try:
390
- start = _time.monotonic()
414
+ start = time.monotonic()
391
415
  while True:
392
- elapsed = _time.monotonic() - start
416
+ elapsed = time.monotonic() - start
393
417
  if elapsed >= duration:
394
418
  break
395
419
  if not self._rtde_c.isConnected():
@@ -254,19 +254,19 @@ class URRobot:
254
254
 
255
255
  logger.info("URRobot initialized at %s", ip)
256
256
 
257
- def activate_gripper(self, *, timeout: float = 10.0) -> bool:
258
- """Activate the gripper with a timeout.
257
+ def _activate_gripper(self, *, timeout: float = 10.0) -> bool:
258
+ """Activate the gripper with error handling (internal use).
259
259
 
260
260
  Tries to activate the configured gripper. If activation fails
261
261
  or times out (e.g., gripper not physically connected), disconnects
262
262
  the gripper, nulls out ``self._gripper``, and returns ``False``.
263
263
 
264
- This is the single place that handles gripper activation — the
265
- CLI and library code should call this rather than touching
266
- ``gripper.activate()`` directly.
264
+ Users should call ``robot.gripper.activate()`` directly instead.
265
+ This method exists for the CLI which needs graceful fallback
266
+ when the gripper isn't connected.
267
267
 
268
268
  Args:
269
- timeout: Maximum seconds to wait for activation (default 5.0).
269
+ timeout: Maximum seconds to wait for activation (default 10.0).
270
270
 
271
271
  Returns:
272
272
  ``True`` if the gripper was activated successfully,
@@ -438,7 +438,7 @@ class URRobot:
438
438
  Example:
439
439
  >>> robot.move_frame = MoveFrame.TOOL
440
440
  >>> robot.move_relative([0.01, 0, 0, 0, 0, 0]) # +X in tool frame
441
- >>> robot.move_to("pick", offset=[0, 0, 0.05]) # offset in tool frame
441
+ >>> robot.move_to("pick", offset_z=0.05) # offset in tool frame
442
442
  """
443
443
  return self._move_frame
444
444
 
@@ -821,6 +821,12 @@ class URRobot:
821
821
  *,
822
822
  offset: list[float] | None = None,
823
823
  frame: MoveFrame | None = None,
824
+ offset_x: float = 0.0,
825
+ offset_y: float = 0.0,
826
+ offset_z: float = 0.0,
827
+ offset_rx: float = 0.0,
828
+ offset_ry: float = 0.0,
829
+ offset_rz: float = 0.0,
824
830
  ) -> list[float]:
825
831
  """Resolve a saved point or raw pose to a TCP pose.
826
832
 
@@ -831,9 +837,16 @@ class URRobot:
831
837
  target: A saved point name (str) or a raw TCP pose
832
838
  [x, y, z, rx, ry, rz].
833
839
  offset: Optional offset [dx, dy, dz, drx, dry, drz]
834
- applied to the target pose.
840
+ applied to the target pose. Mutually exclusive with
841
+ individual offset_* parameters.
835
842
  frame: Coordinate frame for the offset. Falls back to the
836
843
  current ``move_frame`` property (BASE or TOOL).
844
+ offset_x: X offset in meters (default 0.0).
845
+ offset_y: Y offset in meters (default 0.0).
846
+ offset_z: Z offset in meters (default 0.0).
847
+ offset_rx: Roll offset in radians (default 0.0).
848
+ offset_ry: Pitch offset in radians (default 0.0).
849
+ offset_rz: Yaw offset in radians (default 0.0).
837
850
 
838
851
  Returns:
839
852
  TCP pose as [x, y, z, rx, ry, rz].
@@ -843,18 +856,32 @@ class URRobot:
843
856
 
844
857
  Example:
845
858
  >>> pose = robot.get_pose("pick")
846
- >>> pose = robot.get_pose("pick", offset=[0, 0, 0.05, 0, 0, 0])
859
+ >>> pose = robot.get_pose("pick", offset_z=0.05)
847
860
  >>> robot.move_to(pose)
848
861
  """
849
862
  point = self._lookup_point(target)
850
863
 
851
- if offset is not None:
852
- if len(offset) != 6:
864
+ # Build offset from individual params or use provided list
865
+ individual_offset = [offset_x, offset_y, offset_z, offset_rx, offset_ry, offset_rz]
866
+ has_individual = any(v != 0.0 for v in individual_offset)
867
+
868
+ if offset is not None and has_individual:
869
+ raise PointError(
870
+ "Cannot use both 'offset' list and individual offset_* parameters. "
871
+ "Use one or the other."
872
+ )
873
+
874
+ final_offset = offset if offset is not None and not has_individual else (
875
+ individual_offset if has_individual else None
876
+ )
877
+
878
+ if final_offset is not None:
879
+ if len(final_offset) != 6:
853
880
  raise PointError(
854
881
  f"Offset must have 6 values [dx, dy, dz, drx, dry, drz], "
855
- f"got {len(offset)}."
882
+ f"got {len(final_offset)}."
856
883
  )
857
- point = point.with_offset(offset, frame=frame or self._move_frame)
884
+ point = point.with_offset(final_offset, frame=frame or self._move_frame)
858
885
 
859
886
  return list(point.pose)
860
887
 
@@ -868,6 +895,12 @@ class URRobot:
868
895
  vel: float | None = None,
869
896
  acc: float | None = None,
870
897
  asynchronous: bool = False,
898
+ offset_x: float = 0.0,
899
+ offset_y: float = 0.0,
900
+ offset_z: float = 0.0,
901
+ offset_rx: float = 0.0,
902
+ offset_ry: float = 0.0,
903
+ offset_rz: float = 0.0,
871
904
  ) -> None:
872
905
  """Move to a saved point or raw pose.
873
906
 
@@ -877,11 +910,18 @@ class URRobot:
877
910
  linear: If True (default), use Cartesian linear move (moveL).
878
911
  If False, use joint-space move (moveJ).
879
912
  offset: Optional offset [dx, dy, dz, drx, dry, drz]
880
- applied to the target pose before moving.
913
+ applied to the target pose before moving. Mutually
914
+ exclusive with individual offset_* parameters.
881
915
  frame: Coordinate frame for the offset. Falls back to the
882
916
  current ``move_frame`` property (BASE or TOOL).
883
917
  vel: Velocity override. Falls back to default_vel.
884
918
  acc: Acceleration override. Falls back to default_acc.
919
+ offset_x: X offset in meters (default 0.0).
920
+ offset_y: Y offset in meters (default 0.0).
921
+ offset_z: Z offset in meters (default 0.0).
922
+ offset_rx: Roll offset in radians (default 0.0).
923
+ offset_ry: Pitch offset in radians (default 0.0).
924
+ offset_rz: Yaw offset in radians (default 0.0).
885
925
 
886
926
  Raises:
887
927
  MotionError: If the move fails or IK has no solution.
@@ -890,8 +930,9 @@ class URRobot:
890
930
  Example:
891
931
  >>> robot.move_to("home")
892
932
  >>> robot.move_to("pick", linear=False)
893
- >>> robot.move_to("place", offset=[0, 0, 0.05, 0, 0, 0])
894
- >>> robot.move_to("place", offset=[0, 0, 0.05], frame=MoveFrame.TOOL)
933
+ >>> robot.move_to("pick", offset_z=0.05) # 5cm above
934
+ >>> robot.move_to("pick", offset_x=0.01, offset_z=-0.02)
935
+ >>> robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full
895
936
  >>> robot.move_to([0.5, 0, 0.3, 0, 0, 0]) # raw pose
896
937
  """
897
938
  self._check_connection()
@@ -899,14 +940,28 @@ class URRobot:
899
940
 
900
941
  point = self._lookup_point(target)
901
942
 
943
+ # Build offset from individual params or use provided list
944
+ individual_offset = [offset_x, offset_y, offset_z, offset_rx, offset_ry, offset_rz]
945
+ has_individual = any(v != 0.0 for v in individual_offset)
946
+
947
+ if offset is not None and has_individual:
948
+ raise PointError(
949
+ "Cannot use both 'offset' list and individual offset_* parameters. "
950
+ "Use one or the other."
951
+ )
952
+
953
+ final_offset = offset if offset is not None and not has_individual else (
954
+ individual_offset if has_individual else None
955
+ )
956
+
902
957
  # Apply offset if provided
903
- if offset is not None:
904
- if len(offset) != 6:
958
+ if final_offset is not None:
959
+ if len(final_offset) != 6:
905
960
  raise PointError(
906
961
  f"Offset must have 6 values [dx, dy, dz, drx, dry, drz], "
907
- f"got {len(offset)}."
962
+ f"got {len(final_offset)}."
908
963
  )
909
- point = point.with_offset(offset, frame=frame or self._move_frame)
964
+ point = point.with_offset(final_offset, frame=frame or self._move_frame)
910
965
 
911
966
  pose = list(point.pose)
912
967
 
@@ -978,12 +1033,18 @@ class URRobot:
978
1033
 
979
1034
  def move_relative(
980
1035
  self,
981
- delta: list[float],
1036
+ delta: list[float] | None = None,
982
1037
  *,
983
1038
  linear: bool = True,
984
1039
  frame: MoveFrame | None = None,
985
1040
  vel: float | None = None,
986
1041
  acc: float | None = None,
1042
+ delta_x: float = 0.0,
1043
+ delta_y: float = 0.0,
1044
+ delta_z: float = 0.0,
1045
+ delta_rx: float = 0.0,
1046
+ delta_ry: float = 0.0,
1047
+ delta_rz: float = 0.0,
987
1048
  ) -> None:
988
1049
  """Relative Cartesian move from the current position.
989
1050
 
@@ -992,27 +1053,55 @@ class URRobot:
992
1053
 
993
1054
  Args:
994
1055
  delta: [dx, dy, dz, drx, dry, drz] in meters/radians.
1056
+ Mutually exclusive with individual delta_* parameters.
995
1057
  linear: If True (default), use Cartesian linear move.
996
1058
  If False, solve IK and use joint-space move.
997
1059
  frame: Coordinate frame for the delta. Falls back to the
998
1060
  current ``move_frame`` property (BASE or TOOL).
999
1061
  vel: Velocity override. Falls back to default_vel.
1000
1062
  acc: Acceleration override. Falls back to default_acc.
1063
+ delta_x: X delta in meters (default 0.0).
1064
+ delta_y: Y delta in meters (default 0.0).
1065
+ delta_z: Z delta in meters (default 0.0).
1066
+ delta_rx: Roll delta in radians (default 0.0).
1067
+ delta_ry: Pitch delta in radians (default 0.0).
1068
+ delta_rz: Yaw delta in radians (default 0.0).
1001
1069
 
1002
1070
  Raises:
1003
1071
  MotionError: If the move fails.
1004
1072
 
1005
1073
  Example:
1006
- >>> robot.move_relative([0, 0.01, 0, 0, 0, 0]) # 1cm along Y
1007
- >>> robot.move_relative([0, 0, 0.05], frame=MoveFrame.TOOL)
1074
+ >>> robot.move_relative(delta_y=0.01) # 1cm along Y
1075
+ >>> robot.move_relative(delta_z=0.05, frame=MoveFrame.TOOL)
1076
+ >>> robot.move_relative([0, 0.01, 0, 0, 0, 0]) # full list
1008
1077
  """
1009
1078
  self._check_connection()
1010
1079
  self._disable_freedrive_guard()
1011
1080
 
1012
- if len(delta) != 6:
1081
+ # Build delta from individual params or use provided list
1082
+ individual_delta = [delta_x, delta_y, delta_z, delta_rx, delta_ry, delta_rz]
1083
+ has_individual = any(v != 0.0 for v in individual_delta)
1084
+
1085
+ if delta is not None and has_individual:
1086
+ raise MotionError(
1087
+ "Cannot use both 'delta' list and individual delta_* parameters. "
1088
+ "Use one or the other."
1089
+ )
1090
+
1091
+ final_delta = delta if delta is not None and not has_individual else (
1092
+ individual_delta if has_individual else None
1093
+ )
1094
+
1095
+ if final_delta is None:
1096
+ raise MotionError(
1097
+ "Relative move requires either 'delta' list or at least one "
1098
+ "delta_* parameter."
1099
+ )
1100
+
1101
+ if len(final_delta) != 6:
1013
1102
  raise MotionError(
1014
1103
  f"Relative move requires 6 values [dx,dy,dz,drx,dry,drz], "
1015
- f"got {len(delta)}."
1104
+ f"got {len(final_delta)}."
1016
1105
  )
1017
1106
 
1018
1107
  vel = vel if vel is not None else self._default_vel
@@ -1021,7 +1110,7 @@ class URRobot:
1021
1110
 
1022
1111
  try:
1023
1112
  current = list(self._rtde_r.getActualTCPPose())
1024
- target = transform_pose_delta(current, delta, effective_frame)
1113
+ target = transform_pose_delta(current, final_delta, effective_frame)
1025
1114
 
1026
1115
  if linear:
1027
1116
  self._motion.movel(target, vel=vel, acc=acc)
@@ -1113,16 +1202,31 @@ class URRobot:
1113
1202
  except Exception as e:
1114
1203
  raise MotionError(f"move_sequence failed: {e}")
1115
1204
 
1205
+ def zero_ft_sensor(self) -> None:
1206
+ """Zero the robot's force/torque sensor.
1207
+
1208
+ Clears the baseline so that subsequent force/torque readings
1209
+ reflect only forces applied after this call. Call before
1210
+ ``move_until_contact()`` or any force-based operation.
1211
+
1212
+ Raises:
1213
+ MotionError: If the command fails.
1214
+ """
1215
+ self._check_connection()
1216
+ self._motion.zero_ft_sensor()
1217
+
1116
1218
  def move_until_contact(
1117
1219
  self,
1118
1220
  speed_vector: list[float],
1119
1221
  *,
1120
1222
  threshold: float = 5.0,
1121
1223
  acceleration: float = 0.1,
1224
+ zero_first: bool = True,
1122
1225
  ) -> None:
1123
1226
  """Move until contact is detected via TCP force sensing.
1124
1227
 
1125
1228
  Runs an interruptible control loop — press Ctrl+C to stop at any time.
1229
+ Zeros the FT sensor by default before reading the baseline.
1126
1230
 
1127
1231
  Args:
1128
1232
  speed_vector: 6-element speed vector
@@ -1131,9 +1235,11 @@ class URRobot:
1131
1235
  Contact fires when any wrench component changes by more
1132
1236
  than this value from the baseline reading.
1133
1237
  acceleration: Acceleration limit passed to ``speedL()`` in m/s².
1238
+ zero_first: If True (default), zero the FT sensor before reading
1239
+ the baseline. Set to False if you need absolute force values.
1134
1240
 
1135
1241
  Example:
1136
- >>> # Move straight down until contact
1242
+ >>> # Move straight down until contact (zeros FT sensor first)
1137
1243
  >>> robot.move_until_contact([0, 0, -0.02, 0, 0, 0])
1138
1244
  >>> # Higher threshold for heavier contact
1139
1245
  >>> robot.move_until_contact([0, 0, -0.02, 0, 0, 0], threshold=10.0)
@@ -1141,7 +1247,10 @@ class URRobot:
1141
1247
  self._check_connection()
1142
1248
  self._disable_freedrive_guard()
1143
1249
  self._motion.move_until_contact(
1144
- speed_vector, threshold=threshold, acceleration=acceleration
1250
+ speed_vector,
1251
+ threshold=threshold,
1252
+ acceleration=acceleration,
1253
+ zero_first=zero_first,
1145
1254
  )
1146
1255
 
1147
1256
  def move_velocity(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.3.6
3
+ Version: 0.3.8
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -88,7 +88,7 @@ robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
88
88
  robot.gripper.activate()
89
89
 
90
90
  robot.move_to("home")
91
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0, 0])
91
+ robot.move_to("pick", offset_z=0.05) # 5cm above
92
92
  robot.gripper.close()
93
93
  robot.move_to("place")
94
94
  robot.gripper.open()
@@ -394,10 +394,12 @@ A pose is `[x, y, z, rx, ry, rz]`: position in meters and orientation as a **rot
394
394
 
395
395
  #### Offsets
396
396
 
397
- Offsets are 6-element lists `[dx, dy, dz, drx, dry, drz]`:
397
+ Offsets can use individual parameters or a full 6-element list:
398
398
 
399
399
  ```python
400
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0, 0]) # 5cm above pick
400
+ robot.move_to("pick", offset_z=0.05) # 5cm above
401
+ robot.move_to("pick", offset_x=0.01, offset_z=-0.02) # combined
402
+ robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
401
403
  ```
402
404
 
403
405
  #### Resolve a Pose
@@ -416,7 +418,7 @@ robot.move_to(pose) # move to the resolved pose later
416
418
  from urkit import MoveFrame
417
419
 
418
420
  robot.move_frame = MoveFrame.TOOL # default is BASE
419
- robot.move_relative([0, 0, 0.05, 0, 0, 0]) # 5cm along tool Z
421
+ robot.move_relative(delta_z=0.05) # 5cm along tool Z
420
422
  ```
421
423
 
422
424
  - **BASE** (default): delta relative to robot base
@@ -440,8 +442,9 @@ robot.import_points("backup.json")
440
442
  #### Relative Moves
441
443
 
442
444
  ```python
443
- robot.move_relative([0, 0.01, 0, 0, 0, 0]) # 1cm along Y
444
- robot.move_relative([0, 0, 0.05, 0, 0, 0], frame=MoveFrame.TOOL) # 5cm along tool Z
445
+ robot.move_relative(delta_y=0.01) # 1cm along Y
446
+ robot.move_relative(delta_z=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
447
+ robot.move_relative([0, 0.01, 0, 0, 0, 0])
445
448
  ```
446
449
 
447
450
  #### Sequences with Blending
@@ -454,7 +457,17 @@ robot.move_sequence(["a", "b", "c"], blend_radius=0.02)
454
457
  #### Contact Detection
455
458
 
456
459
  ```python
460
+ # Zeros FT sensor automatically, then moves until force exceeds threshold
457
461
  robot.move_until_contact([0, 0, -0.02, 0, 0, 0])
462
+
463
+ # Custom threshold (default: 5.0 N/Nm)
464
+ robot.move_until_contact([0, 0, -0.02, 0, 0, 0], threshold=10.0)
465
+
466
+ # Skip zeroing if you need absolute force values
467
+ robot.move_until_contact([0, 0, -0.02, 0, 0, 0], zero_first=False)
468
+
469
+ # Manual zero (e.g. before custom force-based logic)
470
+ robot.zero_ft_sensor()
458
471
  ```
459
472
 
460
473
  #### Velocity Control
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