Base Backend¶
Abstract base class defining the common interface for all robot control backends.
Overview¶
All backends (Robot, Webots) inherit from RobotBackend and share a common API for joint control and trajectory execution.
from pib3 import Robot, Joint
# All backends support the same methods
with Robot() as backend:
backend.set_joint(Joint.ELBOW_LEFT, 50.0)
pos = backend.get_joint(Joint.ELBOW_LEFT)
backend.run_trajectory("trajectory.json")
RobotBackend Class¶
RobotBackend
¶
Bases: ABC
Abstract base class for robot control backends.
Provides a unified interface for controlling the PIB robot through different backends (Webots simulation, real robot).
Position values use percentage (0-100%) by default, which maps to the joint's calibrated range: - 0% = calibrated minimum angle - 100% = calibrated maximum angle
Use unit="deg" for degrees or unit="rad" for radians.
Implementations
- WebotsBackend: Webots simulator
- RealRobotBackend: Real robot via rosbridge
Example
from pib3 import Joint with backend as robot: ... # Set a single joint (percentage: 0%=min, 100%=max of calibrated range) ... robot.set_joint(Joint.ELBOW_LEFT, 0.0) # 0% = min angle ... robot.set_joint(Joint.ELBOW_LEFT, 50.0) # 50% = middle of range ... robot.set_joint(Joint.ELBOW_LEFT, 100.0) # 100% = max angle ... ... # Use degrees directly ... robot.set_joint(Joint.ELBOW_LEFT, -30.0, unit="deg") # -30 degrees ... angle_deg = robot.get_joint(Joint.ELBOW_LEFT, unit="deg") ... ... # Use radians if needed ... robot.set_joint(Joint.ELBOW_LEFT, 0.5, unit="rad") ... angle_rad = robot.get_joint(Joint.ELBOW_LEFT, unit="rad") ... ... # Save and restore pose (in percentage) ... saved_pose = robot.get_joints() ... robot.set_joints(saved_pose) ... ... # Fire-and-forget (don't wait for completion) ... robot.set_joint(Joint.ELBOW_LEFT, 50.0, async_=True)
Connection Methods¶
connect()¶
Establish connection to the backend.
Must be called before using any other methods. Alternatively, use the context manager.
Example:
from pib3 import Robot, Joint
# Manual connection
robot = Robot(host="172.26.34.149")
robot.connect()
try:
robot.set_joint(Joint.ELBOW_LEFT, 50.0)
finally:
robot.disconnect()
# Context manager (recommended)
with Robot(host="172.26.34.149") as robot:
robot.set_joint(Joint.ELBOW_LEFT, 50.0)
disconnect()¶
Close connection to the backend.
Called automatically when using context manager.
is_connected¶
Property indicating connection status.
Returns: True if connected, False otherwise.
Example:
robot = Robot(host="172.26.34.149")
print(robot.is_connected) # False
robot.connect()
print(robot.is_connected) # True
robot.disconnect()
print(robot.is_connected) # False
Reading Joint Positions¶
get_joint()¶
Get the current position of a single joint.
def get_joint(
self,
motor_name: Union[str, Joint],
unit: Literal["percent", "rad", "deg"] = "percent",
timeout: Optional[float] = None,
) -> Optional[float]
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
motor_name |
str or Joint |
required | Motor name or Joint enum. Must be one of the names in MOTOR_NAMES. |
unit |
"percent", "rad", or "deg" |
"percent" |
Unit for the returned value. "percent" returns 0-100% of the joint's calibrated range, "rad" returns raw radians, "deg" returns degrees. |
timeout |
float or None |
None |
Max time to wait for joint data (seconds). See note below. |
Timeout Behavior by Backend:
| Backend | Default | Behavior |
|---|---|---|
RealRobotBackend |
5.0s | Waits for ROS messages to arrive |
WebotsBackend |
5.0s | Waits for motor readings to stabilize |
Returns: float or None
- Current position in the specified unit
Noneif the position is unavailable (e.g., sensor not ready, timeout expired)
Example:
from pib3 import Robot, Joint
with Robot(host="172.26.34.149") as robot:
# Get position (waits up to 5s by default)
pos_percent = robot.get_joint(Joint.ELBOW_LEFT)
print(f"Elbow at {pos_percent:.1f}%")
# Get position with custom timeout
pos_rad = robot.get_joint(Joint.ELBOW_LEFT, unit="rad", timeout=2.0)
print(f"Elbow at {pos_rad:.3f} rad")
Percentage Requires Calibration
The percentage unit requires calibrated joint limits. See Calibration.
get_joints()¶
Get current positions of multiple joints.
def get_joints(
self,
motor_names: Optional[List[Union[str, Joint]]] = None,
unit: Literal["percent", "rad", "deg"] = "percent",
timeout: Optional[float] = None,
) -> Dict[str, float]
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
motor_names |
List[str\|Joint] or None |
None |
List of motor names (strings or Joint enums) to query. If None, returns all available joints. |
unit |
"percent", "rad", or "deg" |
"percent" |
Unit for the returned values. |
timeout |
float or None |
None |
Max time to wait for joint data (seconds). See get_joint() for backend-specific behavior. |
Timeout Behavior by Backend:
| Backend | Default | Behavior |
|---|---|---|
RealRobotBackend |
5.0s | Waits for ROS messages to arrive |
WebotsBackend |
5.0s | Waits for motor readings to stabilize |
Returns: Dict[str, float]
- Dictionary mapping motor names to their current positions
Example:
from pib3 import Robot
with Robot(host="172.26.34.149") as robot:
# Get all joints (waits up to 5s by default)
all_positions = robot.get_joints()
print(f"Got {len(all_positions)} joint positions")
# Get specific joints with custom timeout
arm_positions = robot.get_joints(
["shoulder_vertical_left", "elbow_left", "wrist_left"],
timeout=2.0,
)
for name, pos in arm_positions.items():
print(f"{name}: {pos:.1f}%")
# Get in radians
arm_rad = robot.get_joints(
["elbow_left", "wrist_left"],
unit="rad"
)
Setting Joint Positions¶
set_joint()¶
Set the position of a single joint.
def set_joint(
self,
motor_name: Union[str, Joint],
position: float,
unit: Literal["percent", "rad", "deg"] = "percent",
async_: bool = False,
timeout: Optional[float] = None,
tolerance: Optional[float] = None,
speed: Optional[float] = None,
) -> bool
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
motor_name |
str or Joint |
required | Motor name or Joint enum. |
position |
float |
required | Target position. For "percent": 0.0 to 100.0. For "rad": angle in radians. For "deg": angle in degrees. |
unit |
"percent", "rad", or "deg" |
"percent" |
Unit for the position value. |
async_ |
bool |
False |
If True, return immediately. If False (default), poll the actual position until it matches the target. |
timeout |
float |
2.0 |
Maximum seconds to wait (only used when async_=False). |
tolerance |
float or None |
None |
Acceptable error for completion. Default: DEFAULT_VERIFY_TOLERANCE_PERCENT (2.0%), DEFAULT_VERIFY_TOLERANCE_DEG (3.0°), or DEFAULT_VERIFY_TOLERANCE (0.05 rad). |
speed |
float or None |
None |
Movement speed in degrees/second (e.g. 90.0 = 90°/s), must be > 0. None uses robot.default_speed (150 deg/s on the robot and in Webots). |
Returns: bool
Trueif the command was sent successfully (and position reached ifasync_=False)Falseif failed
Example:
from pib3 import Robot, Joint
import math
with Robot(host="172.26.34.149") as robot:
# Set using percentage (recommended)
robot.set_joint(Joint.ELBOW_LEFT, 50.0) # Move to 50%
robot.set_joint(Joint.ELBOW_LEFT, 0.0) # Move to minimum
robot.set_joint(Joint.ELBOW_LEFT, 100.0) # Move to maximum
# Set using radians
robot.set_joint(Joint.ELBOW_LEFT, math.pi / 4, unit="rad") # 45 degrees
# Wait for completion
success = robot.set_joint(
Joint.ELBOW_LEFT,
75.0,
async_=False,
timeout=2.0,
tolerance=3.0, # Accept within 3%
)
if success:
print("Joint reached target position")
else:
print("Joint did not reach target in time")
set_joints()¶
Set positions of multiple joints simultaneously.
def set_joints(
self,
positions: Dict[Union[str, Joint], float],
unit: Literal["percent", "rad", "deg"] = "percent",
async_: bool = False,
timeout: Optional[float] = None,
tolerance: Optional[float] = None,
speed: Optional[float] = None,
) -> bool
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
positions |
Dict[str\|Joint, float] |
required | Dict mapping motor names (string or Joint enum) to target positions. A plain dict is required — passing a HandPose raises TypeError (use set_joints_pose() instead); passing a Sequence[float] raises TypeError (use set_joints_sequence() instead). |
unit |
"percent", "rad", or "deg" |
"percent" |
Unit for position values. |
async_ |
bool |
False |
If True, return immediately. If False (default), poll actual positions until they match targets. |
timeout |
float |
2.0 |
Maximum seconds to wait (only used when async_=False). |
tolerance |
float or None |
None |
Acceptable error for completion. |
speed |
float or None |
None |
Movement speed in degrees/second. None uses the backend's default. |
Every command sets its own speed
speed=None means robot.default_speed, not "whatever the last call
left behind". Earlier versions kept the last speed on the Tinkerforge
channel, so after the slow go_home() every later move crawled at
10 deg/s. Slow down a whole program with robot.default_speed = 45.
Timeouts, limits and the return value
timeout=None derives the wait from the speed and the joint's range, so
slow moves no longer "fail" after a fixed 2 s. Targets outside a joint's
range are clamped to the limit (with a one-time hint) and a blocking call
then returns False. Unknown joint names and units raise ValueError
with a suggestion; unit accepts "deg", "degree", "degrees",
"rad", "%", ...
Before pib3 0.2: speed was shared state on the Tinkerforge direct path
On the real-robot direct path, speed rewrote the servo channel's
motion configuration, which is shared state. A later call (even
an async_=True one) that passed a different speed changed
the channel's velocity; subsequent calls that omitted speed kept
using the last value.
Returns: bool
Trueif successfulFalseif failed
Example:
from pib3 import Robot, Joint
with Robot(host="172.26.34.149") as robot:
# Set multiple joints with a dictionary
robot.set_joints({
Joint.SHOULDER_VERTICAL_LEFT: 30.0,
Joint.SHOULDER_HORIZONTAL_LEFT: 40.0,
Joint.ELBOW_LEFT: 60.0,
Joint.WRIST_LEFT: 50.0,
})
# Save and restore poses
saved_pose = robot.get_joints() # Save current pose
# ... move robot around ...
robot.set_joints(saved_pose) # Restore saved pose
# Fire-and-forget
robot.set_joints(
{Joint.ELBOW_LEFT: 50.0, Joint.WRIST_LEFT: 50.0},
async_=True,
)
# Slow motion at 45 deg/s
robot.set_joints({Joint.ELBOW_LEFT: 50.0}, speed=45.0)
set_joints_pose()¶
Apply a HandPose preset to the robot.
def set_joints_pose(
self,
pose: HandPose,
async_: bool = False,
timeout: Optional[float] = None,
tolerance: Optional[float] = None,
speed: Optional[float] = None,
) -> bool
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
pose |
HandPose |
required | A HandPose enum member (e.g. HandPose.LEFT_OPEN). Raises TypeError for any other type. |
async_ |
bool |
False |
If True, fire-and-forget. If False (default), wait for completion. |
timeout |
float |
2.0 |
Max wait time when async_=False. |
tolerance |
float or None |
None |
Position tolerance (in percent — pose values are always percent). |
speed |
float or None |
None |
Movement speed in degrees/second. |
Example:
from pib3 import Robot, HandPose
with Robot(host="172.26.34.149") as robot:
robot.set_joints_pose(HandPose.LEFT_CLOSED)
robot.set_joints_pose(HandPose.RIGHT_OPEN, speed=45.0)
set_joints_sequence()¶
Play a sequence of joint-position waypoints at a fixed rate. Lightweight sibling to run_trajectory() — no IK, no interpolation, no file format.
def set_joints_sequence(
self,
sequence: Sequence[Dict[Union[str, Joint], float]],
unit: Literal["percent", "rad", "deg"] = "percent",
rate_hz: float = 20.0,
progress_callback: Optional[Callable[[int, int], None]] = None,
speed: Optional[float] = None,
) -> bool
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
sequence |
Sequence[Dict[str\|Joint, float]] |
required | Iterable of waypoint dicts. Joints omitted in a given waypoint stay at their last commanded value. |
unit |
"percent", "rad", "deg" |
"percent" |
Unit for all positions. |
rate_hz |
float |
20.0 |
Waypoint dispatch rate. |
progress_callback |
Callable[[int, int], None] or None |
None |
Optional callback(current_index, total). |
speed |
float or None |
None |
Per-waypoint movement speed (deg/s); None = default_speed. |
Waypoints go out on a fixed schedule (command time does not add up). Stops early and returns False on an emergency stop.
Example:
from pib3 import Joint
backend.set_joints_sequence([
{Joint.ELBOW_LEFT: 0.0},
{Joint.ELBOW_LEFT: 50.0},
{Joint.ELBOW_LEFT: 0.0},
], rate_hz=4.0)
Home Position¶
home_percent¶
Property returning the home position (0 radians per joint) expressed as percentages.
For symmetric joints the value is ~50%, but asymmetric joints (e.g. elbow: −45° to +90°) will differ.
go_home()¶
Move all joints to their home position (0 radians — Webots proto zero / real-robot servo midpoint).
def go_home(self, async_: bool = False, timeout: Optional[float] = None, speed: Optional[float] = None) -> bool
Equivalent to set_joints({...: 0.0}, unit="rad") for every motor in MOTOR_NAMES.
Homing starts from an unknown pose
The real robot powers on with its servos un-driven — the arms hang loose — so homing may swing every joint through its full range at once. RealRobotBackend therefore homes very slowly: DEFAULT_HOME_SPEED is 10 deg/s, against ~150 deg/s for normal motion, so the worst case (90° to zero) takes about 9 seconds. Clear the arm radius before calling it.
speed=None (default) uses the backend's DEFAULT_HOME_SPEED; WebotsBackend leaves this at None (simulation starts already at zero, so there is nothing to swing).
with backend as robot:
robot.go_home() # all joints to 0 rad, at the safe homing speed
robot.go_home(speed=90.0) # deliberately faster
Emergency Stop¶
See the Safety page for the classroom view.
In short: the stop latches, freezes the motors where they are, and every
later motion command raises EmergencyStopError until resume(). It arms
itself with a program's first motion command (estop_armed); a program
that only reads the camera never arms it. On the real robot one stop latches
every pib3 program connected to that robot.
stop() / resume() / stopped / stop_reason¶
def stop(self, reason: Optional[str] = None) -> None # freeze + latch, any thread
def resume(self) -> None # deliberate release
@property
def stopped(self) -> bool
@property
def stop_reason(self) -> Optional[str]
stop() freezes every servo at its current ramp position (deceleration
briefly 0, so the bricklet does not brake through a 75° overshoot), ends any
blocking wait, run_trajectory() or set_joints_sequence() at once (they
return False), and makes later motion commands raise
pib3.EmergencyStopError. Pressing a stop key again does not resume.
A program that has moved the robot and ends with an exception or Ctrl+C
inside with Robot(...) freezes the motors as well; a normal end lets the
last moves finish.
enable_estop_key() / disable_estop_key() / estop_keys¶
def enable_estop_key(self, keys: Union[None, str, Sequence[str]] = None) -> bool
def disable_estop_key(self) -> None
@property
def estop_keys(self) -> Tuple[str, ...]
Default keys: Space, Esc, Numpad-0, Pause on the robot
(pib3.DEFAULT_STOP_KEYS), Space in Webots (Webots does not pass Esc to
controllers); pass a name or list for others ("f12", "enter", single
characters). Calling it arms the stop at once; otherwise the first motion
command arms it with the constructor's estop_keys.
On the robot the keys need a global keyboard hook (pynput, one listener
for all robot objects); it returns False and says why when that cannot
work: macOS without Input Monitoring permission, Linux under Wayland, no
display. In Webots the keys come from Webots' own keyboard device, which only
sees key presses while the 3D view has focus.
Fixed in 0.2: Numpad-0 never worked
The old default KeyCode.from_vk(96) never compared equal to a real key
press on Windows or Linux (pynput also compares the scan code / X11
symbol), and on macOS it matched F5 instead of Numpad-0.
show_stop_button() / hide_stop_button()¶
A big red STOP window (separate process, always on top) that opens by itself
when the stop arms and is the visible sign that it is armed. It lists the
ways to stop that work here (click, Space, Esc, Ctrl+C; in Webots "Space in
the 3D view"), turns grey after a stop, and follows the system language
(German/English, PIB3_LANG forces one). Click it, or press Space/Esc while
it has focus. stop_button="auto" shows it only where the keys cannot work.
Ctrl+C¶
While a robot is connected, Ctrl+C first freezes the motors and then raises
the usual KeyboardInterrupt. That works on every system without any
permission (RobotBackend.STOP_ON_CTRL_C).
Trajectory Execution¶
run_trajectory()¶
Execute a trajectory on this backend.
def run_trajectory(
self,
trajectory: Union[str, Path, Trajectory],
rate_hz: float = 20.0,
progress_callback: Optional[Callable[[int, int], None]] = None,
approach_speed: Optional[float] = None,
) -> bool
Before streaming, the arm moves to the first waypoint at approach_speed
(default DEFAULT_APPROACH_SPEED = 30 deg/s; 0 skips it) and waits until it
is there, so the start of a drawing is not smeared. Waypoints outside the
joint limits are clamped with one warning.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
trajectory |
str, Path, or Trajectory |
required | The trajectory to execute. Can be a file path to a JSON file, or a Trajectory object. |
rate_hz |
float |
20.0 |
Playback rate in Hz (waypoints per second). Higher values = faster playback. |
progress_callback |
Callable[[int, int], None] or None |
None |
Optional callback function called after each waypoint. Receives (current_index, total_waypoints). |
Returns: bool
Trueif trajectory completed successfullyFalseif failed or interrupted
Example:
from pib3 import Robot, Trajectory
with Robot(host="172.26.34.149") as robot:
# From file path
robot.run_trajectory("my_trajectory.json")
# From Trajectory object
trajectory = Trajectory.from_json("my_trajectory.json")
robot.run_trajectory(trajectory)
# With custom playback rate
robot.run_trajectory(trajectory, rate_hz=30.0) # Faster
# With progress callback
def on_progress(current, total):
percent = (current / total) * 100
print(f"\rProgress: {percent:.1f}%", end="", flush=True)
robot.run_trajectory(trajectory, progress_callback=on_progress)
print() # Newline after progress
Available Motor Names¶
All 26 motors available on the PIB robot:
Head (2 motors)¶
| Motor Name | Description |
|---|---|
turn_head_motor |
Head rotation (left/right) |
tilt_forward_motor |
Head tilt (up/down) |
Left Arm (6 motors)¶
| Motor Name | Description |
|---|---|
shoulder_vertical_left |
Shoulder raise/lower |
shoulder_horizontal_left |
Shoulder forward/back |
upper_arm_left_rotation |
Upper arm rotation |
elbow_left |
Elbow bend |
lower_arm_left_rotation |
Forearm rotation |
wrist_left |
Wrist bend |
Left Hand (6 motors)¶
| Motor Name | Description |
|---|---|
thumb_left_opposition |
Thumb rotation |
thumb_left_stretch |
Thumb bend |
index_left_stretch |
Index finger bend |
middle_left_stretch |
Middle finger bend |
ring_left_stretch |
Ring finger bend |
pinky_left_stretch |
Pinky finger bend |
Right Arm (6 motors)¶
| Motor Name | Description |
|---|---|
shoulder_vertical_right |
Shoulder raise/lower |
shoulder_horizontal_right |
Shoulder forward/back |
upper_arm_right_rotation |
Upper arm rotation |
elbow_right |
Elbow bend |
lower_arm_right_rotation |
Forearm rotation |
wrist_right |
Wrist bend |
Right Hand (6 motors)¶
| Motor Name | Description |
|---|---|
thumb_right_opposition |
Thumb rotation |
thumb_right_stretch |
Thumb bend |
index_right_stretch |
Index finger bend |
middle_right_stretch |
Middle finger bend |
ring_right_stretch |
Ring finger bend |
pinky_right_stretch |
Pinky finger bend |
Unit System¶
All backends support three units for position values:
| Unit | Value Range | Description |
|---|---|---|
"percent" |
0.0 to 100.0 | Percentage of calibrated joint range (default) |
"rad" |
varies | Raw angle in radians |
"deg" |
varies | Raw angle in degrees |
Percentage is recommended for most use cases:
0%= joint at minimum position50%= joint at midpoint100%= joint at maximum position
from pib3 import Joint
import math
# These are equivalent ways to move to the middle
backend.set_joint(Joint.ELBOW_LEFT, 50.0) # 50%
backend.set_joint(Joint.ELBOW_LEFT, 50.0, unit="percent") # Explicit
# Use radians or degrees for precise angular control
backend.set_joint(Joint.ELBOW_LEFT, math.pi / 4, unit="rad") # 45 degrees
backend.set_joint(Joint.ELBOW_LEFT, -30.0, unit="deg") # -30 degrees
Available Backends¶
| Backend | Class | Import | Use Case |
|---|---|---|---|
| Real Robot | RealRobotBackend |
from pib3 import Robot |
Control physical PIB robot |
| Webots | WebotsBackend | from pib3.backends import WebotsBackend | Webots physics simulation |