Skip to main content

URKit

PyPI

URKit makes it easy to get a Universal Robots e-Series robot moving from Python.

What it is

A thin layer over ur_rtde that handles the common stuff: connecting, teaching named points, moving between them, gripper control, telemetry. Raw RTDE interfaces are exposed for anything deeper.

No .urp files, no Dashboard API, no extra programs to run on the robot. Just connect over the network and go. Handles power-on, brake release, and RTDE setup in the constructor so you don't have to.

Comes with an interactive teach pendant CLI for positioning the robot and saving waypoints, plus a Python API for scripting motion, gripper control, I/O, and telemetry.

When to use this

Projects where the robot is part of something bigger. Computer vision, machine learning, sensor fusion, data logging. If your project lives in Python, keep the robot control in Python too.

Built for labs and research setups where you need to get the robot moving fast and integrate it with other software. Not designed as a drop-in replacement for Polyscope in production cells, but perfectly capable for anything that runs from a PC.

How it works

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.

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.


Table of Contents


Quick Start

pip install -U urkit

The -U (upgrade) flag ensures you always get the latest version. This project is in early development and changes frequently.

Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).

Platform Support

  • Linux, Windows: fully supported. The interactive CLIs (urkit teach, urkit points) are tested on both platforms
  • macOS: runs the same POSIX terminal code path as Linux (termios/select); there is no macOS-specific code

Robot Setup (one-time)

  1. Network: Connect your PC to the robot via Ethernet cable (direct) or a switch.

    First, configure the robot: ☰ → System → System → Network. Set a static IP, e.g., 192.168.1.50 / Subnet 255.255.255.0.

    Then set a matching static IP on your PC. The exact IP can be anything, as long as the first three octets match the robot's network and the last octet is different:

    Linux:

    • Open Settings → Network → click the gear icon next to Wired
    • Go to IPv4 tab → change Automatic (DHCP) to Manual
    • Enter Address: 192.168.1.1, Netmask: 255.255.255.0
    • Click Apply, then toggle the connection off and on

    macOS:

    • Open System Settings → Network → click Ethernet (or your adapter)
    • Click Details... next to the adapter name → TCP/IP
    • Set Configure IPv4 to Manually
    • Enter IP: 192.168.1.1, Subnet Mask: 255.255.255.0
    • Click OK

    Windows:

    • Open Settings → Network & internet → Ethernet
    • Click the adapter name (e.g., Ethernet) at the top
    • Under IP settings, click Edit → select Manual
    • Toggle IPv4 On, enter IP: 192.168.1.1, Subnet mask: 255.255.255.0
    • Click Save

    Verify connectivity: ping 192.168.1.50.

  2. Remote Control: ☰ → System → Remote Control: Enable. Press the remote/local button on the pendant.

  3. Security: ☰ → Security → Services: enable RTDE and disable EtherNet/IP, PROFINET, or MODBUS if they're claiming RTDE registers. Save and restart.

That's it. No .urp files to run, no extra programs needed.

Example

If you have a Robotiq gripper, install the Robotiq Gripper Control URCap first: download from robotiq.com/support, copy the .urcap to a USB drive, and install via ☰ → Settings → System → URCaps.

from urkit import URRobot, ROBOTIQ_HAND_E  # or ROBOTIQ_2F_85, ROBOTIQ_2F_140, ROBOTIQ_EPICK_SINGLE_S, or gripper=None

robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
robot.gripper.activate()

robot.move_to("home")
robot.move_to("pick", dz=0.05)  # 5cm above
robot.gripper.close()
robot.set_held_payload(0.05)    # picked up 50 g: arm compensates gravity
robot.move_to("place")
robot.gripper.open()
robot.set_held_payload(0)       # released: back to gripper-only payload

The typical workflow:

  1. Scaffold config. Run urkit init to create a default config.yaml.
  2. Edit it. Set your robot IP, gripper type, and other settings.
  3. Teach points. Run urkit teach (auto-loads config) to position the robot and save waypoints.
  4. Write code. Load the config and move to points by name:
from urkit import URRobot

robot = URRobot.from_config("config.yaml")
robot.move_to("home")
robot.move_to("pick", dz=0.05)
  1. Iterate. Add more points, tweak your code, repeat.

Configuration

URKit uses a YAML config file (config.yaml) to persist settings between sessions.

Create a Config File

Generate a default config with all options documented:

urkit init                          # writes config.yaml in CWD
urkit init --output station_a.yaml  # custom path
urkit init --force                  # overwrite if exists

Edit the generated file with your robot IP and gripper settings.

Location

The CLI auto-loads config.yaml from the current working directory. Pass --config for a custom path.

Keys

Key Description Example
robot_ip Robot IP address 192.168.1.50
points_path Path to SQLite points database points.db
gripper Gripper preset name hand-e, 2f-85, 2f-140, epick-single-s, digital
ik_reference IK reference posture (prevents elbow flipping) home
default_vel Default linear velocity (m/s) 0.5
default_acc Default linear acceleration (m/s²) 0.3
expert_mode Disable safety speed clamping false

The loader fails loudly with a reason when a config file exists but is wrong: malformed YAML (file + parse error), a document that is not a mapping, or unknown top-level keys (offending keys + the valid ones; usually a typo). The table above is the complete key set. The only silent case is a missing file, where the CLI runs on flags and built-in defaults.

Gripper Config

Built-in preset with overrides:

gripper: hand-e
gripper_config:
  force: 50
  speed: 80

Digital I/O gripper:

gripper: digital
gripper_config:
  pin: 3
  close_on_high: true

Custom gripper (arbitrary payload + TCP offset, no backend):

gripper:
  mass: 0.5
  center_of_gravity: [0.0, 0.0, 0.0]
  tcp_offset: [0.0, 0.0, 0.175, 0.0, 0.0, 0.0]
  backend: none

Override physical properties on a built-in preset:

gripper: 2f-85
gripper_config:
  mass: 1.2
  center_of_gravity: [0.0, 0.0, 0.07]
  tcp_offset: [0.0, 0.0, 0.200, 0.0, 0.0, 0.0]

CLI Override Precedence

  1. CLI flags. urkit teach 192.168.1.50 --gripper none
  2. Config file. Values from config.yaml
  3. Built-in defaults. points.db, no gripper, 0.5 m/s velocity

Saving Config

The CLI never modifies your config file automatically. Press Y inside the teach pendant to save. This way you only save settings you've actually tested.

urkit teach 192.168.1.50 --gripper hand-e  # test, then press Y
urkit teach                               # next time: reads from config

Multiple workcells:

urkit teach --config station_a.yaml   # press Y to save
urkit teach --config station_b.yaml   # separate config

Programmatic

from urkit import URRobot

robot = URRobot.from_config("config.yaml")
robot = URRobot.from_config("config.yaml", ip="10.0.0.50")  # override IP
robot = URRobot.from_config({"robot_ip": "192.168.1.50", "gripper": "2f-85"})  # dict, no file

Interactive CLI

URKit provides three CLI tools: init to scaffold a config file, teach for interactive robot control, and points for browsing saved waypoints.

Init

Generate a default config file with all options documented:

urkit init                          # writes config.yaml in CWD
urkit init --output station_a.yaml  # custom path
urkit init --force                  # overwrite if exists

Teach Mode

The interactive teach pendant for moving the robot, saving points, and checking telemetry:

urkit teach 192.168.1.50             # with robot IP
urkit teach                          # reads IP from config.yaml

Flags:

Flag Description
ip Robot IP address (positional, overrides config)
--gripper Gripper preset: 2f-85, 2f-140, hand-e, epick-single-s, digital, none
--gripper-pin Digital gripper output pin (default: 0)
--gripper-force 2F grip force 0-100 (EPick: use max_vacuum in gripper_config)
--gripper-speed 2F movement speed 0-100 (EPick: use grip_timeout in gripper_config)
--gripper-close-on-high Digital polarity: true or false
--points Path to points.db file (overrides config)
--config Path to config file (default: config.yaml in project root or CWD)
-e, --expert Disable safety speed clamping (full speed for goto/tcp-down)
-v, --verbose Show verbose output (debug connection issues)

Points Explorer

Browse saved waypoints with real-time search filtering. No robot connection needed:

urkit points                          # uses default points.db
urkit points test_points.db           # use specific database

Key Map

All movement and orientation keys support hold-to-repeat.

Movement Orientation Step Size
KeyAction
W / S+X / -X
A / D+Y / -Y
Q / E+Z / -Z
KeyAction
U / ORoll - / +
I / KPitch - / +
J / LYaw - / +
KeyAction
1Set linear step (mm)
2Set angular step (deg)
.Reset defaults (5 mm / 2°)
Gripper Points Controls
KeyAction
XOpen
CClose
VSet position % (2F only; hidden for the EPick)
6Set speed (2F) / min vacuum (EPick)
7Set force (2F) / max vacuum (EPick)
9Set held payload (kg, 0 = reset to base)
KeyAction
BSave current position
GGo to saved point (Space to cancel mid-move, ESC to exit)
HDelete saved point
POpen points explorer
RRename saved point
KeyAction
FFreedrive toggle (ALL ↔ XYZ)
3Freedrive axis menu (toggle individual axes)
MToggle frame (BASE / TOOL)
NGo To mode (Cartesian / Joint)
TOpen TCP orient submenu (6 directions)
YSave config to file
ESCExit

Joint Display

The teach pendant shows live joint angles alongside TCP position and orientation:

 Position (mm) X= +432.0  Y= +111.0  Z= +227.0
 Orientation   R= +131.3  P= -121.0  Y=  +8.0
 Joints        J1=+150.0  J2= +20.0  J3=+160.0
               J4= +50.0  J5= -80.0  J6=+157.0

Joint angles color-code proximity to mechanical limits:

  • Yellow: within 10% of joint range (warning)
  • Red: within 5% of joint range (danger)

UR e-Series joint limits:

Joint Range
J1 (shoulder pan) ±360°
J2 (shoulder lift) ±360°
J3 (elbow) ±360°
J4 (wrist 1) ±360°
J5 (wrist 2) ±360°
J6 (wrist 3) ±360°

Safety

By default, Go To and TCP orient movements use a slow velocity (0.125 m/s) so its safer for anyone standing near the robot. The user's speed slider still applies as a global multiplier on top of this.

Delta movements (W/S/A/D/Q/E) use step-size-based velocities that scale with the speed slider set by the user.

To disable the slow default and use full speed, pass --expert or set expert_mode: true in your config:

urkit teach 192.168.1.50 --expert
# config.yaml
expert_mode: true

API Reference

Connecting

from urkit import URRobot, ROBOTIQ_HAND_E

robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)

With custom motion defaults:

robot = URRobot(
    ip="192.168.1.50",
    points="points.db",
    gripper=ROBOTIQ_HAND_E,
    default_vel=0.5,    # m/s
    default_acc=0.3,    # m/s²
)

See Configuration for from_config() usage.

The constructor takes a few seconds on first call: it validates the connection, checks remote mode, powers on the robot, releases brakes, and connects RTDE. Subsequent calls are faster if the robot is already running.

Grippers

Built-in presets:

Preset Description
ROBOTIQ_HAND_E Robotiq Hand-E (2F-85-E)
ROBOTIQ_2F_85 Robotiq 2F-85
ROBOTIQ_2F_140 Robotiq 2F-140
ROBOTIQ_EPICK_SINGLE_S/L EPick + 1 cup (S = Ø40, L = Ø55)
ROBOTIQ_EPICK_DUAL_S/L EPick + 2-cup bracket (S = Ø40, L = Ø55)
ROBOTIQ_EPICK_QUAD_S/L EPick + 4-cup bracket (S = Ø40, L = Ø55)
robot.gripper.activate()              # required before open/close (Robotiq only)
robot.gripper.deactivate()            # deactivate (Robotiq only)
robot.gripper.is_activated()          # check activation state

robot.gripper.open()                  # fully open (blocking by default)
robot.gripper.close()                 # fully closed, stops on contact
robot.gripper.open(wait=False)        # non-blocking return (waits for position)
robot.gripper.set_position_mm(20)     # 20mm open (Robotiq only, 0 = fully closed)
robot.gripper.set_position_percent(50) # 50% open (2F; repurposed on the EPick, see below)
robot.gripper.set_force(50)           # grip force: 0-100 (Robotiq only)
robot.gripper.set_speed(80)           # movement speed: 0-100 (Robotiq only)

robot.gripper.get_position_mm()       # last commanded position in mm
robot.gripper.max_travel_mm()         # max finger travel (e.g. 85.0 for 2F-85)

Override preset values for custom fingers:

robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E, max_mm=120)

Override physical properties (e.g., custom fingers or added hardware change the weight):

robot = URRobot(
    ip="192.168.1.50",
    points="points.db",
    gripper=ROBOTIQ_HAND_E,
    mass=1.2,                          # override preset mass
    center_of_gravity=[0.0, 0.0, 0.07], # override CoG
    tcp_offset=[0.0, 0.0, 0.180, 0, 0, 0],  # override TCP offset
)

Digital I/O Grippers

Robotiq grippers use a serial protocol over the robot's RS485 port. If you have a suction cup, solenoid, or any actuator controlled by a single digital output pin, use DigitalGripperConfig instead. It just turns that pin on (close) and off (open).

from urkit import URRobot, DigitalGripperConfig

robot = URRobot(
    ip="192.168.1.50",
    points="points.db",
    gripper=DigitalGripperConfig(pin=3),  # pin 3 goes high = close
)

robot.gripper.open()    # turn pin 3 off
robot.gripper.close()   # turn pin 3 on

set_force() and set_speed() are not available for digital grippers.

EPick Vacuum Gripper

The EPick presets drive a Robotiq EPick vacuum gripper (e-Series only) over the same RS485 Robotiq protocol as the 2F series, using the legacy Robotiq ASCII commands. They require the Robotiq Gripper Control URCap installed (same setup as the 2F presets).

The semantics are inverted compared to the 2F grippers:

robot.gripper.activate()          # required before open/close
robot.gripper.close()             # grip: pump on, wait until an object is secured
robot.gripper.open()              # release: pump off + blow-off, wait until the object is gone

close() does not return until the gripper has confirmed a secured object (see Detection below), and raises GripperError if no object is detected within the grip timeout. A failed grip leaves the vacuum on, so call open() to release before moving again. open() waits until the vacuum is lost (the object has dropped).

Grip parameters (set at connect time, or changed between grips):

robot.gripper.set_max_vacuum(60)    # max vacuum level, 20-100% (default 60, the Robotiq URCap default)
robot.gripper.set_min_vacuum(40)    # min vacuum level, 10% up to the max vacuum (default 40)
robot.gripper.set_grip_timeout(30)  # grip timeout in 100 ms units, 0-100 (default 30 = 3 s)

The ranges are enforced at construction and on every setter (max 20-100, min 10 up to the current max, timeout 0-100), and a parameter of one backend passed to another raises GripperError instead of being ignored (force/speed on the EPick, max_vacuum/grip_timeout on the 2F). The same names work in config.yaml:

gripper: epick-single-s
gripper_config:
  max_vacuum: 70
  grip_timeout: 40

With set_max_vacuum(100) the pump runs continuously and the gripper cannot confirm a grip; keep the max vacuum below 100% (60 works for typical cups and trays). The same physics applies at lower levels: if the max vacuum is higher than a leaky seal can reach, the pump keeps hunting and never enters the stable hold phase where the secured flag appears, so the grip times out (verified: a marginal-seal object gripped at 60/40 but timed out at 80/50 and 80/30). For leaky seals keep max_vacuum low and min_vacuum at or below what the seal sustains, not the other way around.

set_position_mm(), get_position_mm(), and max_travel_mm() are not available: the EPick has no finger travel. set_position_percent(p) is repurposed as a one-shot vacuum grip: p=0 releases, 1-100 grips at that vacuum level with the normal secured-object wait (the teach pendant does not expose this; use [6]/[7] and C).

Payload and TCP. The bare generator body is 0.706 kg, CoG [0, 0, 0.0515], TCP [0, 0, 0.113, 0, 0, 0] (Robotiq EPick manual, Table 6-4); cups and brackets change all three. The kits ship cups in two sizes (Ø 40 mm and Ø 55 mm), so there is one preset per bracket system + cup size (configurations without cups cannot grip and have no preset):

Preset Config / CLI name Setup
ROBOTIQ_EPICK_SINGLE_S epick-single-s 1 small (Ø40) cup on the generator port
ROBOTIQ_EPICK_SINGLE_L epick-single-l 1 large (Ø55) cup on the generator port
ROBOTIQ_EPICK_DUAL_S epick-dual-s 2-cup bracket, small cups
ROBOTIQ_EPICK_DUAL_L epick-dual-l 2-cup bracket, large cups
ROBOTIQ_EPICK_QUAD_S epick-quad-s 4-cup bracket, small cups
ROBOTIQ_EPICK_QUAD_L epick-quad-l 4-cup bracket, large cups
from urkit import URRobot, ROBOTIQ_EPICK_SINGLE_S

robot = URRobot(ip="192.168.1.50", gripper=ROBOTIQ_EPICK_SINGLE_S)

The TCP z of each preset is the bottom of the cups, the limiting point for approach moves. The presets use the manual's Table 6-4 for the body and bracket stack, and the official Festo datasheets for the cups Robotiq ships (VASB-40-1/4-PUR-B: 30 g / 32 mm, VASB-55-1/4-PUR-B: 39 g / 39 mm); all-small combinations reproduce Table 6-4 exactly:

Preset Mass (kg) CoG z (m) TCP z (m)
body only (reference) 0.706 0.0515 0.113
ROBOTIQ_EPICK_SINGLE_S 0.736 0.0547 0.145
ROBOTIQ_EPICK_SINGLE_L 0.745 0.0557 0.152
ROBOTIQ_EPICK_DUAL_S 1.116 0.0819 0.196
ROBOTIQ_EPICK_DUAL_L 1.134 0.0837 0.203
ROBOTIQ_EPICK_QUAD_S 1.341 0.0920 0.196
ROBOTIQ_EPICK_QUAD_L 1.377 0.0947 0.203

Bracket mass is derived (±15 g) and the Ø55 height is CAD-derived (±2 mm); if your setup is critical, measure the actual stack and pass mass / center_of_gravity / tcp_offset overrides, which still work:

robot = URRobot(
    ip="192.168.1.50",
    gripper=ROBOTIQ_EPICK_SINGLE_S,
    mass=0.74,
    tcp_offset=[0.0, 0.0, 0.147, 0.0, 0.0, 0.0],
)

Detection. The EPick runs in advanced mode (rMOD=1): the pump reaches the max vacuum level, stops, and the vacuum decays back to the min level. An object is only "secured" while the vacuum is actually held there, so close() returns as soon as that secured state is observed on the gripper's object flag. With nothing under the suction cup the vacuum collapses and the secured state never occurs, so close() times out and raises GripperError after the grip timeout (set via grip_timeout). In automatic mode (rMOD=0) the EPick reports a short "seemingly constant" vacuum as an object, which false-positives with no object on the cup, so urkit does not use it.

Points & Motion

The points database is optional. Create a robot without one and attach later:

robot = URRobot(ip="192.168.1.50")
robot.points_db = "points.db"

Moving to Points

robot.move_to("pick")                      # linear move (default)
robot.move_to("pick", linear=False)        # joint move
robot.move_to("pick", vel=1.0, acc=0.5)    # override speed
robot.move_to("pick", asynchronous=True)   # non-blocking, returns immediately
robot.move_to([0.5, 0, 0.3, 0, 0, 0])      # raw pose (no points DB needed)
  • Linear (moveL): TCP moves in a straight line. Predictable path, slower near complex orientations.
  • Joint (moveJ): Each joint moves simultaneously. Faster, but the TCP follows an arc.
  • Asynchronous: When asynchronous=True, the move runs in the UR controller's background thread and the method returns immediately.

Non-Blocking Moves

Start a move and do other things while it runs:

robot.move_to("pick", asynchronous=True)

# Poll until done
while robot.is_moving():
    time.sleep(0.01)

# Or cancel mid-move (see [Speed Control](#speed-control) for `stop()`)
robot.move_to("pick", asynchronous=True)
while robot.is_moving():
    if should_cancel:
        robot.stop()
        break
    time.sleep(0.01)

Teach pendant Go To uses this pattern internally. Space cancels the move and returns to the menu.

Pose Format

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.

Deltas

Deltas can use individual parameters or a full 6-element list:

robot.move_to("pick", dz=0.05)  # 5cm above
robot.move_to("pick", dx=0.01, dz=-0.02)  # combined
robot.move_to("pick", delta=[0, 0, 0.05, 0, 0.1, 0])  # full with rotation

Resolve a Pose

Get a pose without moving. Useful for logging, comparisons, or custom motion:

point = robot.get_point("pick")
point = robot.get_point("pick", delta=[0, 0, 0.05, 0, 0, 0])  # with delta
robot.move_to(pose)  # move to the resolved pose later

Coordinate Frame

from urkit import MoveFrame

robot.move_frame = MoveFrame.TOOL   # default is BASE
robot.move_relative(dz=0.05)  # 5cm along tool Z
  • BASE (default): delta relative to robot base
  • TOOL: delta relative to TCP orientation

IK Reference

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.

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.

# 1. Put the robot in your preferred posture (e.g., "home")
# 2. Save it: robot.save_point("home")
# 3. Set as IK reference:
robot.ik_reference = "home"

# Now all moves stay close to that posture, no elbow flipping
robot.move_to("pick")
robot.move_to("place")
robot.move_relative(dz=-0.05)

In config.yaml (recommended for permanent setup):

robot_ip: 192.168.1.50
ik_reference: home    # prevents weird elbow/wrist flips

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. Under the hood, poses are resolved to joint angles and sent as moveJ instead of moveL.

Per-move override:

robot.ik_reference = "home"                    # global default
robot.move_to("weird_pose", ik_reference=None) # one move without it
robot.move_to("back", ik_reference="current") # use current joints

When to use it:

  • Use it for sequences of positional moves between waypoints: pick/place paths, assembly sequences, anything where the robot travels between distant points. It prevents elbow/wrist flipping.
  • Don't use it for rotation-heavy movements (orientation adjustments, fine-tuning angles). The qnear seed can push joints into unexpected configs for pure rotations, and you lose the controller's native Cartesian trajectory planner (lookahead, smoothing). Set ik_reference=None for these moves.

Default is None (controller handles IK natively). Set it globally when most of your moves are positional, and override per-move when you need rotations.

Point Management

Points are stored as absolute TCP poses in the robot base frame. When you move to a point, the controller moves whatever TCP is currently active to that base-frame pose, so the tool tip lands at the same physical location. Change the TCP offset (or swap grippers) and your saved points stay valid.

robot.save_point("here")
robot.point_names()           # ["home", "pick", "place"]
robot.rename_point("old", "new")
robot.delete_point("old")
robot.export_points("backup.json")
robot.import_points("backup.json")

Relative Moves

robot.move_relative(dy=0.01)  # 1cm along Y
robot.move_relative(dz=0.05, frame=MoveFrame.TOOL)  # 5cm along tool Z
robot.move_relative([0, 0.01, 0, 0, 0, 0])  # full 6-element delta

Individual delta parameters (dx, dy, dz, drx, dry, drz) are mutually exclusive with the delta list. Use one or the other.

Sequences

# Both styles work:
robot.move_sequence("a", "b", "c")          # variadic
robot.move_sequence(["a", "b", "c"])        # list

move_sequence builds a path from all targets and executes it in a single call with blending. Requires at least 2 targets. Two modes:

With ik_reference (positional paths): All poses resolve to joints using chained inverse kinematics. The first pose resolves relative to the reference, the second relative to the first's resolved joints, and so on. Executed as a single moveJ(path) call. Keeps the arm configuration consistent (no elbow flipping):

robot.ik_reference = "home"
robot.move_sequence(["a", "b", "c"], blend_radius=0.02)  # 2cm blend

Without ik_reference (Cartesian path): Builds a blended moveL(path). The controller handles IK natively, better for rotation-heavy sequences where ik_reference causes unexpected joint behavior:

robot.move_sequence(["a", "b", "c"], blend_radius=0.02)  # blended Cartesian path

Before sending any move, it checks that each pose has a valid IK solution. Unreachable poses raise MotionError. If a named point doesn't exist, it raises PointError instead of silently falling back.

Contact Detection

# Move straight down until contact (zeros FT sensor automatically)
robot.move_until_contact(vz=-0.02)

# Custom threshold (default: 5.0 N/Nm)
robot.move_until_contact(vz=-0.02, threshold=10.0)

# Skip zeroing if you need absolute force values
robot.move_until_contact(vz=-0.02, zero_first=False)

# Full speed vector
robot.move_until_contact([0, 0, -0.02, 0, 0, 0])

# Abort after 10s or 200mm of travel, whichever comes first
robot.move_until_contact(vz=-0.02, timeout=10.0, max_distance=0.2)

# Returns True if contact detected, False if timeout/max_distance hit
for _ in range(3):
    if robot.move_until_contact(vz=-0.02, timeout=10.0, max_distance=0.2):
        break  # contact detected
    # No contact, back off and retry
    robot.move_relative(dz=0.01)

# Manual zero (e.g. before custom force-based logic)
robot.zero_ft_sensor()

Velocity Control

robot.move_velocity([0, 0, -0.02, 0, 0, 0], duration=1.0)

Freedrive Mode

from urkit import FreedriveMode

robot.enable_freedrive()              # all 6 axes free
robot.enable_freedrive(FreedriveMode.XYZ)      # linear axes only (X, Y, Z)
robot.enable_freedrive(FreedriveMode.ROTATION) # rotation only (Roll, Pitch, Yaw)
robot.enable_freedrive([1, 1, 1, 1, 0, 0])    # custom: X+Y+Z+Roll+Pitch
robot.disable_freedrive()             # disable before sending motion commands
robot.is_freedrive_active             # check state

Teach pendant: Press F to toggle freedrive (ALL ↔ XYZ). Press 3 to open the axis selection menu arrow keys navigate, space toggles each axis on/off, enter applies.

Speed Control

robot.stop()                          # stop current move immediately (stopL + stopJ)
robot.speed_stop()                    # stop velocity-controlled motion (not E-stop)
robot.speed_slider = 0.5           # 50% velocity cap
robot.speed_slider              # read current slider (0.0-1.0)

The speed slider controls the pendant's speed multiplier. It's global, persistent (stored on the robot, survives restarts), and affects all motion commands. Because 0% persists as a silent freeze of all motion, the pendant's [0] prompt rejects 0 (1-100 only); a deliberate freeze is still possible via the API (robot.speed_slider = 0).

Changing Default Speed & Acceleration

Override the constructor defaults at runtime. All subsequent moves pick up the new values:

robot.default_vel = 0.1   # slow for precision work
robot.move_to("insert")
robot.default_vel = 0.5   # back to normal

robot.default_acc = 0.05    # gentle acceleration

robot.default_vel      # read current velocity (m/s)
robot.default_acc      # read current acceleration (m/s²)

Inverse Kinematics

joints = robot.inverse_kinematics([0.5, 0, 0.3, 0, 0, 0])

Telemetry

pose = robot.get_current_point()           # [x, y, z, rx, ry, rz]
pose = robot.get_current_point(dz=0.05)  # with delta
joints = robot.get_joint_positions()       # [j0..j5]
force = robot.get_tcp_force()              # [fx, fy, fz, mx, my, mz]
mode = robot.get_robot_mode()              # "REMOTE_CONTROL", "SERVOING", etc.
payload = robot.payload                    # kg
robot.is_protective_stopped()              # bool
robot.is_emergency_stopped()               # bool

Arrival Detection

is_moving() tracks the target pose or joints stored by move_to() and compares against the current position. Returns False when within tolerance:

robot.move_to("pick", asynchronous=True)

# Wait until arrived (default tolerances: 2mm position, ~2° orientation)
while robot.is_moving():
    time.sleep(0.01)

# Tighter tolerances
while robot.is_moving(position_tolerance=0.001, orientation_tolerance=0.017):
    time.sleep(0.01)

# Joint moves, tighter joint tolerance
while robot.is_moving(joint_tolerance=0.001):
    time.sleep(0.01)

For joint-space moves (when ik_reference is active or linear=False), is_moving() compares joint angles. For Cartesian moves, it compares TCP pose. When no target is set (e.g., after a relative move without move_to), it returns False.


More API

Less common but useful when you need them.

Digital I/O

Pins 0–7 are standard, 8–15 configurable, 16–17 tool.

# Outputs
robot.set_digital_output(0, True)
robot.set_digital_outputs({0: True, 1: False, 8: True})
robot.set_digital_outputs(False)      # clear all pins 0–15
robot.get_digital_output(0)           # read back output state

# Inputs
robot.get_digital_input(0)
robot.wait_for_input(0, True, timeout=10.0)  # block until pin 0 goes high

# Analog
robot.get_analog_input(0)             # read analog input (pin 0–1)
robot.get_analog_output(0)            # read analog output (pin 0–1)

# Tool I/O
robot.get_tool_input(0)               # tool digital input (pin 0–1)
robot.get_tool_output(0)              # tool digital output (pin 0–1)

Geometry

Conversion utilities for rotation vectors, quaternions, and RPY angles (rxyz convention, same as UR teach pendant):

from urkit import (
    orient_tcp,
    orient_tcp_down,
    quat_to_rotvec,
    quat_to_rpy,
    rpy_to_quat,
    rotvec_to_quat,
)

# Orient TCP along an arbitrary direction (minimal rotation)
new_pose = orient_tcp(current_pose, [0, 0, -1])  # point down
new_pose = orient_tcp(current_pose, [1, 0, 0])   # point forward

# Orient TCP straight down (convenience wrapper)
new_pose = orient_tcp_down(current_pose)

# Rotation conversions
q = rotvec_to_quat([0, 0, 1.57])   # rotvec → quaternion (x, y, z, w)
rv = quat_to_rotvec(q)              # quaternion → rotvec
rpy = quat_to_rpy(q)                # quaternion → RPY (radians)
q = rpy_to_quat(0, 0, 1.57)        # RPY → quaternion

Robot Lifecycle

Manual power and brake control via the Dashboard:

robot.power_on()          # power on (skips if already on)
robot.release_brakes()    # release brakes (enable control)
robot.recover()           # clear protective stop
robot.power_off()         # power off

The constructor handles all of this automatically. Use these methods when you need fine-grained control (e.g., recovering from a safety stop without recreating the robot).

TCP and payload can be set manually (gripper presets do this automatically):

robot.tcp_offset = [0, 0, 0.15, 0, 0, 0]
robot.set_payload(1.5, [0, 0, 0.05])  # mass (kg), center of gravity [x, y, z]

Held payload. The controller compensates gravity using the payload, so add the mass of a picked-up object before moving it, and reset after release:

robot.gripper.close()
robot.set_held_payload(0.05)                        # holding 50 g, object CoG at the TCP
robot.set_held_payload(0.05, cog=[0, 0, 0.25])      # object hangs below the flange
robot.set_held_payload(0)                           # released: back to the base payload

robot.base_payload     # gripper + tooling, kg (no held object)
robot.held_payload     # currently held mass, kg (0 = none)
robot.payload          # total (base + held), kg

set_held_payload(0) resets to the base payload captured at connect (the gripper preset plus any mass= / center_of_gravity / tcp_offset overrides), never to a literal zero. The combined CoG is the mass-weighted average of the base CoG and the object's CoG (tool coordinates, origin at the flange). A held object that extends the tool does not need a TCP change: give its CoG the deeper z instead.

The base payload can be changed at runtime with set_payload(...): while nothing is held, a manual set_payload call becomes the new base (custom fingers, a re-weighed tool). While a payload is held, set_payload raises, since the mass would be ambiguous: call set_held_payload(0) first.

In the teach pendant, 9 sets the held payload (prompt: Held payload in kg (0 = reset to base)); the Payload status row shows the held mass and hints the key, e.g. 1.427kg +0.050 held [9]. Useful when teaching points while gripping a real part.

How much to report. There is no enforced minimum, and over-reporting is harmless. Practical guidance: below ~10 g the sag is sub-millimeter on any e-Series arm and not worth it; from ~50 g up (roughly 1% of a UR5e's rated payload) report it, since full-reach sag reaches several mm; in between, report it for slow precision work (insertion, contact moves) and skip it for coarse moves.

Clean shutdown:

robot.disconnect()  # close RTDE, Dashboard, points DB, gripper

disconnect() is called automatically on garbage collection. Call it explicitly to release resources immediately.


Advanced

Raw RTDE Access

URKit doesn't try to wrap everything. Access the raw ur_rtde interfaces for advanced features:

robot.rtde_control.forceMode(...)
robot.rtde_control.servoJ(...)
robot.rtde_receive.getActualCurrent()

Full ur_rtde documentation: https://sdurobotics.gitlab.io/ur_rtde/

Connection Monitoring

robot.connection_lost       # bool: check if RTDE dropped

An RTDE drop is fatal for the session: urkit raises and the CLI exits. Restart urkit (or create a new URRobot) to reconnect. urkit deliberately does not auto-reconnect mid-session: a silent reconnect hides what happened to the link and lets the native layer resynchronize state behind your back.

What happens when the link drops mid-session (verified by force-dropping the network with Windows firewall rules):

  • Before a motion call: every motion command checks the link first and raises URKitConnectionError immediately. This matters because a call into the native layer on a dead link would not return an error: the vendored ur_rtde library enters its own internal reconnect loop and blocks indefinitely (measured: 45 s+ and still hanging under a firewall block).
  • While a motion is running: the CLI's connection monitor detects the drop within ~0.1 s and exits cleanly (interrupt watcher on Windows, SIGALRM on Unix). In library code, an in-flight call can block inside the native reconnect loop; kill the process and restart.
  • After a hard drop: the robot can keep the RTDE registers locked until the dead connection times out. If a fresh connect fails with RtdeRegisterConflictError, power cycle the controller (dashboard power off / power on works remotely; or Settings -> System -> Shutdown on the pendant), then reconnect.

Error Handling

from urkit import URKitError, MotionError, PointError

try:
    robot = URRobot(ip="192.168.1.50", points="points.db")
except RobotNotInRemoteModeError:
    print("Enable remote control on the teach pendant!")
except RtdeRegisterConflictError:
    print("Disable EtherNet/IP, PROFINET, or MODBUS!")
except URKitError as e:
    print(f"Error: {e}")

Common runtime errors:

Exception When
MotionError Unreachable pose, bad TCP offset, freedrive failure
PointError Point not found, no points database configured
GripperError Gripper activation or communication failure
URKitIOError Invalid pin number, I/O read/write failure
TelemetryError Cannot read pose, joints, force, etc.

When the robot enters protective stop or the RTDE connection drops, motion commands raise URKitConnectionError and the program should exit. The CLI handles this automatically.


Changelog

See CHANGELOG.md for the full version history.

Metadata

Release files for urkit 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for urkit 0.6.0
File Size Uploaded
urkit-0.6.0.tar.gz 168.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for urkit 0.6.0
File Interpreter ABI Platform
urkit-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 294.8 kB

Release files / urkit-0.6.0.tar.gz

Download URL urkit-0.6.0.tar.gz
Size 168.1 kB
Tags Source
SHA-256 checksum
How to use checksums
869dc4a1be0690f47973af110692564110d01ed226a1392470154777fa19d130
BLAKE2b-256 checksum
How to use checksums
2a416382130fad5ca763b4ac0dc813c639360dab06999c38425f7d2ea2930711
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / urkit-0.6.0-py3-none-any.whl

Download URL urkit-0.6.0-py3-none-any.whl
Size 126.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cb4e0c121ea781901785e299b8d8100ca1e91f4c63dcca416f83123f720bff82
BLAKE2b-256 checksum
How to use checksums
96125e414d9ee23395d27f442de1ceaae513d81934ce2816973e3af21336345e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.26

2 release files

0.3.25

2 release files

0.3.24

2 release files

0.3.23

2 release files

0.3.22

2 release files

0.3.21

2 release files

0.3.20

2 release files

0.3.19

2 release files

0.3.18

2 release files

0.3.17

2 release files

0.3.16

2 release files

0.3.15

2 release files

0.3.14

2 release files

0.3.13

2 release files

0.3.12

2 release files

0.3.11

2 release files

0.3.10

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page