urkit 0.4.6__tar.gz → 0.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.
Files changed (47) hide show
  1. {urkit-0.4.6 → urkit-0.5.0}/PKG-INFO +47 -23
  2. {urkit-0.4.6 → urkit-0.5.0}/README.md +46 -22
  3. {urkit-0.4.6 → urkit-0.5.0}/pyproject.toml +1 -1
  4. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/__init__.py +4 -2
  5. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/__main__.py +8 -0
  6. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/cli/teach.py +161 -62
  7. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/cli/terminal.py +188 -33
  8. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/geometry.py +2 -1
  9. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/motion.py +82 -30
  10. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/points.py +9 -9
  11. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/robot.py +268 -224
  12. {urkit-0.4.6 → urkit-0.5.0}/src/urkit.egg-info/PKG-INFO +47 -23
  13. {urkit-0.4.6 → urkit-0.5.0}/src/urkit.egg-info/SOURCES.txt +6 -1
  14. urkit-0.5.0/tests/test_connection_gate.py +104 -0
  15. urkit-0.5.0/tests/test_freedrive_cycle.py +96 -0
  16. urkit-0.5.0/tests/test_ik_reference.py +236 -0
  17. urkit-0.5.0/tests/test_message_line.py +93 -0
  18. {urkit-0.4.6 → urkit-0.5.0}/tests/test_move_sequence.py +5 -4
  19. {urkit-0.4.6 → urkit-0.5.0}/tests/test_points.py +5 -5
  20. {urkit-0.4.6 → urkit-0.5.0}/tests/test_robot_integration.py +2 -2
  21. urkit-0.5.0/tests/test_terminal_input.py +247 -0
  22. {urkit-0.4.6 → urkit-0.5.0}/setup.cfg +0 -0
  23. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/cli/__init__.py +0 -0
  24. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/cli/colors.py +0 -0
  25. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/cli/connection_monitor.py +0 -0
  26. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/cli/init.py +0 -0
  27. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/cli/points.py +0 -0
  28. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/config.py +0 -0
  29. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/connection.py +0 -0
  30. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/exceptions.py +0 -0
  31. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/gripper/__init__.py +0 -0
  32. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/gripper/base.py +0 -0
  33. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/gripper/digital.py +0 -0
  34. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/gripper/presets.py +0 -0
  35. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/gripper/robotiq.py +0 -0
  36. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/gripper/robotiq_preamble.py +0 -0
  37. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/io.py +0 -0
  38. {urkit-0.4.6 → urkit-0.5.0}/src/urkit/telemetry.py +0 -0
  39. {urkit-0.4.6 → urkit-0.5.0}/src/urkit.egg-info/dependency_links.txt +0 -0
  40. {urkit-0.4.6 → urkit-0.5.0}/src/urkit.egg-info/entry_points.txt +0 -0
  41. {urkit-0.4.6 → urkit-0.5.0}/src/urkit.egg-info/requires.txt +0 -0
  42. {urkit-0.4.6 → urkit-0.5.0}/src/urkit.egg-info/top_level.txt +0 -0
  43. {urkit-0.4.6 → urkit-0.5.0}/tests/test_exceptions.py +0 -0
  44. {urkit-0.4.6 → urkit-0.5.0}/tests/test_geometry.py +0 -0
  45. {urkit-0.4.6 → urkit-0.5.0}/tests/test_gripper.py +0 -0
  46. {urkit-0.4.6 → urkit-0.5.0}/tests/test_gripper_factory.py +0 -0
  47. {urkit-0.4.6 → urkit-0.5.0}/tests/test_gripper_presets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.4.6
3
+ Version: 0.5.0
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -46,7 +46,7 @@ Built for labs and research setups where you need to get the robot moving fast a
46
46
 
47
47
  ## How it works
48
48
 
49
- Use the CLI to position the robot and save named waypoints. Then reference them by name in your code: move to points, apply offsets, run sequences. Points are stored in a local SQLite database, no robot-side setup needed.
49
+ Use the CLI to position the robot and save named waypoints. Then reference them by name in your code: move to points, apply deltas, run sequences. Points are stored in a local SQLite database, no robot-side setup needed.
50
50
 
51
51
  The typical workflow: teach points with the pendant, write a few lines of Python to string them together, run it. Add vision, add sensors, add logic. The robot is just one component in your pipeline.
52
52
 
@@ -144,7 +144,7 @@ robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
144
144
  robot.gripper.activate()
145
145
 
146
146
  robot.move_to("home")
147
- robot.move_to("pick", offset_z=0.05) # 5cm above
147
+ robot.move_to("pick", dz=0.05) # 5cm above
148
148
  robot.gripper.close()
149
149
  robot.move_to("place")
150
150
  robot.gripper.open()
@@ -162,7 +162,7 @@ from urkit import URRobot
162
162
 
163
163
  robot = URRobot.from_config("config.yaml")
164
164
  robot.move_to("home")
165
- robot.move_to("pick", offset_z=0.05)
165
+ robot.move_to("pick", dz=0.05)
166
166
  ```
167
167
 
168
168
  5. **Iterate.** Add more points, tweak your code, repeat.
@@ -592,14 +592,14 @@ while robot.is_moving():
592
592
 
593
593
  A pose is `[x, y, z, rx, ry, rz]`: position in meters and orientation as a **rotation vector** (axis-angle in radians). This is not RPY (roll/pitch/yaw). The teach pendant displays RPY in degrees, which is a different representation. Values you see on the pendant won't match `get_current_point()` directly.
594
594
 
595
- #### Offsets
595
+ #### Deltas
596
596
 
597
- Offsets can use individual parameters or a full 6-element list:
597
+ Deltas can use individual parameters or a full 6-element list:
598
598
 
599
599
  ```python
600
- robot.move_to("pick", offset_z=0.05) # 5cm above
601
- robot.move_to("pick", offset_x=0.01, offset_z=-0.02) # combined
602
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
600
+ robot.move_to("pick", dz=0.05) # 5cm above
601
+ robot.move_to("pick", dx=0.01, dz=-0.02) # combined
602
+ robot.move_to("pick", delta=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
603
603
  ```
604
604
 
605
605
  #### Resolve a Pose
@@ -608,7 +608,7 @@ Get a pose without moving. Useful for logging, comparisons, or custom motion:
608
608
 
609
609
  ```python
610
610
  point = robot.get_point("pick")
611
- point = robot.get_point("pick", offset=[0, 0, 0.05, 0, 0, 0]) # with offset
611
+ point = robot.get_point("pick", delta=[0, 0, 0.05, 0, 0, 0]) # with delta
612
612
  robot.move_to(pose) # move to the resolved pose later
613
613
  ```
614
614
 
@@ -618,7 +618,7 @@ robot.move_to(pose) # move to the resolved pose later
618
618
  from urkit import MoveFrame
619
619
 
620
620
  robot.move_frame = MoveFrame.TOOL # default is BASE
621
- robot.move_relative(delta_z=0.05) # 5cm along tool Z
621
+ robot.move_relative(dz=0.05) # 5cm along tool Z
622
622
  ```
623
623
 
624
624
  - **BASE** (default): delta relative to robot base
@@ -639,7 +639,7 @@ robot.ik_reference = "home"
639
639
  # Now all moves stay close to that posture, no elbow flipping
640
640
  robot.move_to("pick")
641
641
  robot.move_to("place")
642
- robot.move_relative(delta_z=-0.05)
642
+ robot.move_relative(dz=-0.05)
643
643
  ```
644
644
 
645
645
  **In config.yaml** (recommended for permanent setup):
@@ -682,12 +682,12 @@ robot.import_points("backup.json")
682
682
  #### Relative Moves
683
683
 
684
684
  ```python
685
- robot.move_relative(delta_y=0.01) # 1cm along Y
686
- robot.move_relative(delta_z=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
685
+ robot.move_relative(dy=0.01) # 1cm along Y
686
+ robot.move_relative(dz=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
687
687
  robot.move_relative([0, 0.01, 0, 0, 0, 0]) # full 6-element delta
688
688
  ```
689
689
 
690
- Individual delta parameters (`delta_x`, `delta_y`, `delta_z`, `delta_rx`, `delta_ry`, `delta_rz`) are mutually exclusive with the `delta` list. Use one or the other.
690
+ Individual delta parameters (`dx`, `dy`, `dz`, `drx`, `dry`, `drz`) are mutually exclusive with the `delta` list. Use one or the other.
691
691
 
692
692
  #### Sequences
693
693
 
@@ -718,26 +718,26 @@ Before sending any move, it checks that each pose has a valid IK solution. Unrea
718
718
 
719
719
  ```python
720
720
  # Move straight down until contact (zeros FT sensor automatically)
721
- robot.move_until_contact(speed_z=-0.02)
721
+ robot.move_until_contact(vz=-0.02)
722
722
 
723
723
  # Custom threshold (default: 5.0 N/Nm)
724
- robot.move_until_contact(speed_z=-0.02, threshold=10.0)
724
+ robot.move_until_contact(vz=-0.02, threshold=10.0)
725
725
 
726
726
  # Skip zeroing if you need absolute force values
727
- robot.move_until_contact(speed_z=-0.02, zero_first=False)
727
+ robot.move_until_contact(vz=-0.02, zero_first=False)
728
728
 
729
729
  # Full speed vector
730
730
  robot.move_until_contact([0, 0, -0.02, 0, 0, 0])
731
731
 
732
732
  # Abort after 10s or 200mm of travel, whichever comes first
733
- robot.move_until_contact(speed_z=-0.02, timeout=10.0, max_distance=0.2)
733
+ robot.move_until_contact(vz=-0.02, timeout=10.0, max_distance=0.2)
734
734
 
735
735
  # Returns True if contact detected, False if timeout/max_distance hit
736
736
  for _ in range(3):
737
- if robot.move_until_contact(speed_z=-0.02, timeout=10.0, max_distance=0.2):
737
+ if robot.move_until_contact(vz=-0.02, timeout=10.0, max_distance=0.2):
738
738
  break # contact detected
739
739
  # No contact, back off and retry
740
- robot.move_relative(delta_z=0.01)
740
+ robot.move_relative(dz=0.01)
741
741
 
742
742
  # Manual zero (e.g. before custom force-based logic)
743
743
  robot.zero_ft_sensor()
@@ -800,7 +800,7 @@ joints = robot.inverse_kinematics([0.5, 0, 0.3, 0, 0, 0])
800
800
 
801
801
  ```python
802
802
  pose = robot.get_current_point() # [x, y, z, rx, ry, rz]
803
- pose = robot.get_current_point(offset_z=0.05) # with offset
803
+ pose = robot.get_current_point(dz=0.05) # with delta
804
804
  joints = robot.get_joint_positions() # [j0..j5]
805
805
  force = robot.get_tcp_force() # [fx, fy, fz, mx, my, mz]
806
806
  mode = robot.get_robot_mode() # "REMOTE_CONTROL", "SERVOING", etc.
@@ -937,9 +937,33 @@ Full `ur_rtde` documentation: <https://sdurobotics.gitlab.io/ur_rtde/>
937
937
 
938
938
  ```python
939
939
  robot.connection_lost # bool: check if RTDE dropped
940
- robot.reconnect_rtde() # reconnect after a drop
941
940
  ```
942
941
 
942
+ An RTDE drop is fatal for the session: urkit raises and the CLI exits.
943
+ Restart `urkit` (or create a new `URRobot`) to reconnect. urkit
944
+ deliberately does not auto-reconnect mid-session: a silent reconnect
945
+ hides what happened to the link and lets the native layer resynchronize
946
+ state behind your back.
947
+
948
+ **What happens when the link drops mid-session** (verified by
949
+ force-dropping the network with Windows firewall rules):
950
+
951
+ - **Before a motion call**: every motion command checks the link first
952
+ and raises `URKitConnectionError` immediately. This matters because a
953
+ call into the native layer on a dead link would not return an error:
954
+ the vendored `ur_rtde` library enters its own internal reconnect
955
+ loop and blocks indefinitely (measured: 45 s+ and still hanging
956
+ under a firewall block).
957
+ - **While a motion is running**: the CLI's connection monitor detects
958
+ the drop within ~0.1 s and exits cleanly (interrupt watcher on
959
+ Windows, SIGALRM on Unix). In library code, an in-flight call can
960
+ block inside the native reconnect loop; kill the process and restart.
961
+ - **After a hard drop**: the robot can keep the RTDE registers locked
962
+ until the dead connection times out. If a fresh connect fails with
963
+ `RtdeRegisterConflictError`, power cycle the controller (dashboard
964
+ `power off` / `power on` works remotely; or Settings -> System ->
965
+ Shutdown on the pendant), then reconnect.
966
+
943
967
  ### Error Handling
944
968
 
945
969
  ```python
@@ -20,7 +20,7 @@ Built for labs and research setups where you need to get the robot moving fast a
20
20
 
21
21
  ## How it works
22
22
 
23
- Use the CLI to position the robot and save named waypoints. Then reference them by name in your code: move to points, apply offsets, run sequences. Points are stored in a local SQLite database, no robot-side setup needed.
23
+ Use the CLI to position the robot and save named waypoints. Then reference them by name in your code: move to points, apply deltas, run sequences. Points are stored in a local SQLite database, no robot-side setup needed.
24
24
 
25
25
  The typical workflow: teach points with the pendant, write a few lines of Python to string them together, run it. Add vision, add sensors, add logic. The robot is just one component in your pipeline.
26
26
 
@@ -118,7 +118,7 @@ robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
118
118
  robot.gripper.activate()
119
119
 
120
120
  robot.move_to("home")
121
- robot.move_to("pick", offset_z=0.05) # 5cm above
121
+ robot.move_to("pick", dz=0.05) # 5cm above
122
122
  robot.gripper.close()
123
123
  robot.move_to("place")
124
124
  robot.gripper.open()
@@ -136,7 +136,7 @@ from urkit import URRobot
136
136
 
137
137
  robot = URRobot.from_config("config.yaml")
138
138
  robot.move_to("home")
139
- robot.move_to("pick", offset_z=0.05)
139
+ robot.move_to("pick", dz=0.05)
140
140
  ```
141
141
 
142
142
  5. **Iterate.** Add more points, tweak your code, repeat.
@@ -566,14 +566,14 @@ while robot.is_moving():
566
566
 
567
567
  A pose is `[x, y, z, rx, ry, rz]`: position in meters and orientation as a **rotation vector** (axis-angle in radians). This is not RPY (roll/pitch/yaw). The teach pendant displays RPY in degrees, which is a different representation. Values you see on the pendant won't match `get_current_point()` directly.
568
568
 
569
- #### Offsets
569
+ #### Deltas
570
570
 
571
- Offsets can use individual parameters or a full 6-element list:
571
+ Deltas can use individual parameters or a full 6-element list:
572
572
 
573
573
  ```python
574
- robot.move_to("pick", offset_z=0.05) # 5cm above
575
- robot.move_to("pick", offset_x=0.01, offset_z=-0.02) # combined
576
- robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
574
+ robot.move_to("pick", dz=0.05) # 5cm above
575
+ robot.move_to("pick", dx=0.01, dz=-0.02) # combined
576
+ robot.move_to("pick", delta=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
577
577
  ```
578
578
 
579
579
  #### Resolve a Pose
@@ -582,7 +582,7 @@ Get a pose without moving. Useful for logging, comparisons, or custom motion:
582
582
 
583
583
  ```python
584
584
  point = robot.get_point("pick")
585
- point = robot.get_point("pick", offset=[0, 0, 0.05, 0, 0, 0]) # with offset
585
+ point = robot.get_point("pick", delta=[0, 0, 0.05, 0, 0, 0]) # with delta
586
586
  robot.move_to(pose) # move to the resolved pose later
587
587
  ```
588
588
 
@@ -592,7 +592,7 @@ robot.move_to(pose) # move to the resolved pose later
592
592
  from urkit import MoveFrame
593
593
 
594
594
  robot.move_frame = MoveFrame.TOOL # default is BASE
595
- robot.move_relative(delta_z=0.05) # 5cm along tool Z
595
+ robot.move_relative(dz=0.05) # 5cm along tool Z
596
596
  ```
597
597
 
598
598
  - **BASE** (default): delta relative to robot base
@@ -613,7 +613,7 @@ robot.ik_reference = "home"
613
613
  # Now all moves stay close to that posture, no elbow flipping
614
614
  robot.move_to("pick")
615
615
  robot.move_to("place")
616
- robot.move_relative(delta_z=-0.05)
616
+ robot.move_relative(dz=-0.05)
617
617
  ```
618
618
 
619
619
  **In config.yaml** (recommended for permanent setup):
@@ -656,12 +656,12 @@ robot.import_points("backup.json")
656
656
  #### Relative Moves
657
657
 
658
658
  ```python
659
- robot.move_relative(delta_y=0.01) # 1cm along Y
660
- robot.move_relative(delta_z=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
659
+ robot.move_relative(dy=0.01) # 1cm along Y
660
+ robot.move_relative(dz=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
661
661
  robot.move_relative([0, 0.01, 0, 0, 0, 0]) # full 6-element delta
662
662
  ```
663
663
 
664
- Individual delta parameters (`delta_x`, `delta_y`, `delta_z`, `delta_rx`, `delta_ry`, `delta_rz`) are mutually exclusive with the `delta` list. Use one or the other.
664
+ Individual delta parameters (`dx`, `dy`, `dz`, `drx`, `dry`, `drz`) are mutually exclusive with the `delta` list. Use one or the other.
665
665
 
666
666
  #### Sequences
667
667
 
@@ -692,26 +692,26 @@ Before sending any move, it checks that each pose has a valid IK solution. Unrea
692
692
 
693
693
  ```python
694
694
  # Move straight down until contact (zeros FT sensor automatically)
695
- robot.move_until_contact(speed_z=-0.02)
695
+ robot.move_until_contact(vz=-0.02)
696
696
 
697
697
  # Custom threshold (default: 5.0 N/Nm)
698
- robot.move_until_contact(speed_z=-0.02, threshold=10.0)
698
+ robot.move_until_contact(vz=-0.02, threshold=10.0)
699
699
 
700
700
  # Skip zeroing if you need absolute force values
701
- robot.move_until_contact(speed_z=-0.02, zero_first=False)
701
+ robot.move_until_contact(vz=-0.02, zero_first=False)
702
702
 
703
703
  # Full speed vector
704
704
  robot.move_until_contact([0, 0, -0.02, 0, 0, 0])
705
705
 
706
706
  # Abort after 10s or 200mm of travel, whichever comes first
707
- robot.move_until_contact(speed_z=-0.02, timeout=10.0, max_distance=0.2)
707
+ robot.move_until_contact(vz=-0.02, timeout=10.0, max_distance=0.2)
708
708
 
709
709
  # Returns True if contact detected, False if timeout/max_distance hit
710
710
  for _ in range(3):
711
- if robot.move_until_contact(speed_z=-0.02, timeout=10.0, max_distance=0.2):
711
+ if robot.move_until_contact(vz=-0.02, timeout=10.0, max_distance=0.2):
712
712
  break # contact detected
713
713
  # No contact, back off and retry
714
- robot.move_relative(delta_z=0.01)
714
+ robot.move_relative(dz=0.01)
715
715
 
716
716
  # Manual zero (e.g. before custom force-based logic)
717
717
  robot.zero_ft_sensor()
@@ -774,7 +774,7 @@ joints = robot.inverse_kinematics([0.5, 0, 0.3, 0, 0, 0])
774
774
 
775
775
  ```python
776
776
  pose = robot.get_current_point() # [x, y, z, rx, ry, rz]
777
- pose = robot.get_current_point(offset_z=0.05) # with offset
777
+ pose = robot.get_current_point(dz=0.05) # with delta
778
778
  joints = robot.get_joint_positions() # [j0..j5]
779
779
  force = robot.get_tcp_force() # [fx, fy, fz, mx, my, mz]
780
780
  mode = robot.get_robot_mode() # "REMOTE_CONTROL", "SERVOING", etc.
@@ -911,9 +911,33 @@ Full `ur_rtde` documentation: <https://sdurobotics.gitlab.io/ur_rtde/>
911
911
 
912
912
  ```python
913
913
  robot.connection_lost # bool: check if RTDE dropped
914
- robot.reconnect_rtde() # reconnect after a drop
915
914
  ```
916
915
 
916
+ An RTDE drop is fatal for the session: urkit raises and the CLI exits.
917
+ Restart `urkit` (or create a new `URRobot`) to reconnect. urkit
918
+ deliberately does not auto-reconnect mid-session: a silent reconnect
919
+ hides what happened to the link and lets the native layer resynchronize
920
+ state behind your back.
921
+
922
+ **What happens when the link drops mid-session** (verified by
923
+ force-dropping the network with Windows firewall rules):
924
+
925
+ - **Before a motion call**: every motion command checks the link first
926
+ and raises `URKitConnectionError` immediately. This matters because a
927
+ call into the native layer on a dead link would not return an error:
928
+ the vendored `ur_rtde` library enters its own internal reconnect
929
+ loop and blocks indefinitely (measured: 45 s+ and still hanging
930
+ under a firewall block).
931
+ - **While a motion is running**: the CLI's connection monitor detects
932
+ the drop within ~0.1 s and exits cleanly (interrupt watcher on
933
+ Windows, SIGALRM on Unix). In library code, an in-flight call can
934
+ block inside the native reconnect loop; kill the process and restart.
935
+ - **After a hard drop**: the robot can keep the RTDE registers locked
936
+ until the dead connection times out. If a fresh connect fails with
937
+ `RtdeRegisterConflictError`, power cycle the controller (dashboard
938
+ `power off` / `power on` works remotely; or Settings -> System ->
939
+ Shutdown on the pendant), then reconnect.
940
+
917
941
  ### Error Handling
918
942
 
919
943
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "urkit"
7
- version = "0.4.6"
7
+ version = "0.5.0"
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.4.6"
27
+ __version__ = "0.5.0"
28
28
 
29
29
 
30
30
  from urkit.exceptions import (
@@ -58,7 +58,7 @@ from urkit.gripper.presets import (
58
58
  ROBOTIQ_HAND_E,
59
59
  )
60
60
  from urkit.motion import FreedriveMode
61
- from urkit.robot import URRobot
61
+ from urkit.robot import GLOBAL, URRobot
62
62
 
63
63
  __all__ = [
64
64
  # Version
@@ -66,6 +66,8 @@ __all__ = [
66
66
 
67
67
  # Core class
68
68
  "URRobot",
69
+ # IK reference sentinel (per-call ik_reference=GLOBAL)
70
+ "GLOBAL",
69
71
  # Gripper
70
72
  "Gripper",
71
73
  "GripperPreset",
@@ -142,6 +142,14 @@ def main() -> None:
142
142
 
143
143
  def main_entry() -> None:
144
144
  """Entry point for console script."""
145
+ # A redirected Windows console uses the ANSI code page (cp1252):
146
+ # unencodable characters (status symbols, log text) must degrade
147
+ # to '?' instead of crashing the session. TTY consoles are
148
+ # Unicode-native and unaffected.
149
+ for stream in (sys.stdout, sys.stderr):
150
+ reconfigure = getattr(stream, "reconfigure", None)
151
+ if reconfigure is not None:
152
+ reconfigure(errors="replace")
145
153
  main()
146
154
 
147
155