Direct Motor Control (Low-Latency Mode)¶
Bypass ROS for direct Tinkerforge motor control with significantly reduced latency.
Overview¶
The standard motor control path goes through:
This adds ~100-200ms latency per command. Direct mode bypasses ROS:
Reducing latency to ~5-20ms for both reading and writing motor positions.
Direct Tinkerforge control is the default mode. Servo bricklets are auto-discovered on connect. ROS is still connected for audio, camera, and AI subsystems.
When to Use Each Mode¶
| Mode | Latency | Best For |
|---|---|---|
| Direct (default) | 5-20ms | Real-time control, high-frequency updates, interactive demos |
| ROS | 100-200ms | Trajectory playback, non-time-critical operations |
Prerequisites¶
Install the Tinkerforge Python bindings:
Ensure the robot's Tinkerforge Brick Daemon is accessible (port 4223).
Quick Start¶
Direct mode is the default -- no extra configuration needed:
from pib3 import Robot, Joint
with Robot(host="172.26.34.149") as robot:
# Motor commands go directly to Tinkerforge (auto-discovered)
robot.set_joint(Joint.ELBOW_LEFT, 50.0)
angle = robot.get_joint(Joint.ELBOW_LEFT)
To use ROS for motor control instead:
from pib3 import Robot
with Robot(host="172.26.34.149", motor_mode="ros") as robot:
robot.set_joint("elbow_left", 50.0) # Via ROS/rosbridge
Runtime Control¶
Enable/Disable at Runtime¶
with Robot(host="172.26.34.149") as robot:
# Check status
print(f"Low-latency available: {robot.low_latency_available}")
print(f"Low-latency enabled: {robot.low_latency_enabled}")
# Disable for specific operations
robot.low_latency_enabled = False
robot.run_trajectory("trajectory.json") # Uses ROS
# Re-enable
robot.low_latency_enabled = True
Reading Positions¶
When direct mode is enabled, get_joint() and get_joints() also read directly from the Tinkerforge servo bricklets instead of waiting for ROS messages.
Automatic Direct Reading¶
with Robot(host="172.26.34.149") as robot:
# Both reading and writing use direct Tinkerforge control
robot.set_joint("elbow_left", 0.5, unit="rad") # Direct write
pos = robot.get_joint("elbow_left", unit="rad") # Direct read (~5ms)
Reading Multiple Joints¶
# Read all mapped motors directly
positions = robot.get_joints(unit="rad")
# Read specific motors
arm_positions = robot.get_joints(
["elbow_left", "wrist_left", "shoulder_vertical_left"],
unit="rad",
)
Mixed Mode Reading¶
Motors not in the Tinkerforge mapping fall back to ROS:
positions = robot.get_joints([
"elbow_left", # Direct (in mapping)
"turn_head_motor", # Falls back to ROS (not in mapping)
], unit="rad")
Actual vs. Commanded Position
Direct reads return the actual measured position from the servo's potentiometer feedback, not the last commanded position. This is useful for:
- Detecting mechanical issues (motor slippage, loose gears)
- Sensing external forces on the arm (collision detection)
- Verifying the arm reached its target position
- Implementing compliant control where the arm responds to physical interaction
Advanced Configuration¶
LowLatencyConfig¶
For advanced use cases, you can customize the Tinkerforge connection
settings via RobotConfig:
from pib3 import LowLatencyConfig, RobotConfig
from pib3.backends import RealRobotBackend
config = RobotConfig(
host="172.26.34.149",
low_latency=LowLatencyConfig(
enabled=True, # Default: True
tinkerforge_host=None, # Defaults to robot host IP
tinkerforge_port=4223, # Standard Tinkerforge port
motor_mapping=None, # None = auto-discover (recommended)
sync_to_ros=True, # Update local cache for get_joint()
command_timeout=0.5, # Command timeout in seconds
),
)
robot = RealRobotBackend.from_config(config)
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
True |
Enable direct Tinkerforge control |
tinkerforge_host |
str |
None |
Tinkerforge daemon host (defaults to robot IP) |
tinkerforge_port |
int |
4223 |
Tinkerforge daemon port |
motor_mapping |
dict |
None |
Maps motor names to (bricklet_uid, channel). None = auto-discover. |
sync_to_ros |
bool |
True |
Update local position cache after commands |
command_timeout |
float |
0.5 |
Timeout for direct commands |
sync_to_ros Explained¶
When sync_to_ros=True (default), the local position cache is updated after each direct command. This ensures get_joint() returns the correct value.
No ROS Topic Updates
Direct commands do not publish to ROS topics. The position will not be visible in ROS tools like rostopic echo. This is intentional - publishing to /joint_trajectory would trigger a second motor command.
Manual Motor Mapping¶
If auto-discovery doesn't work for your setup, you can provide an explicit motor mapping:
from pib3 import Robot, build_motor_mapping
with Robot(host="172.26.34.149") as robot:
# Build mapping from known servo bricklet UIDs
mapping = build_motor_mapping(
servo1_uid="ABC1", # Right arm bricklet
servo2_uid="ABC2", # Shoulder verticals bricklet
servo3_uid="ABC3", # Left arm + hand bricklet
)
robot.configure_motor_mapping(mapping)
Custom Servo Configuration¶
Servo channels are automatically configured with default settings. To customize:
Configure Individual Motor¶
robot.configure_servo_channel(
"elbow_left",
pulse_width_min=700, # Minimum PWM pulse (microseconds)
pulse_width_max=2500, # Maximum PWM pulse (microseconds)
velocity=9000, # Max velocity (0.01 deg/s) = 90 deg/s
acceleration=9000, # Acceleration (0.01 deg/s^2)
deceleration=9000, # Deceleration (0.01 deg/s^2)
)
Configure All Motors¶
robot.configure_all_servo_channels(
pulse_width_min=700,
pulse_width_max=2500,
velocity=12000, # Faster: 120 deg/s
acceleration=15000,
deceleration=15000,
)
Helper Functions¶
build_motor_mapping()¶
Creates a complete motor mapping from three servo bricklet UIDs:
from pib3 import build_motor_mapping
mapping = build_motor_mapping(
servo1_uid="ABC1", # Controls right arm motors
servo2_uid="ABC2", # Controls shoulder vertical motors (both arms)
servo3_uid="ABC3", # Controls left arm + left hand motors
)
This uses the standard PIB wiring where:
- Servo 1: Right arm (shoulder horizontal, elbow, rotation, hand)
- Servo 2: Both shoulder verticals
- Servo 3: Left arm + left hand
PIB_SERVO_CHANNELS¶
Reference for standard PIB channel assignments:
from pib3 import PIB_SERVO_CHANNELS
# See which bricklet and channel each motor uses: (bricklet_number, channel)
print(PIB_SERVO_CHANNELS["elbow_left"]) # (3, 8)
print(PIB_SERVO_CHANNELS["wrist_left"]) # (3, 6)
Fallback Behavior¶
Motors not in the Tinkerforge mapping automatically fall back to ROS control:
# If only left arm is mapped, right arm uses ROS
robot.set_joints({
"elbow_left": 0.5, # Direct (in mapping)
"elbow_right": 0.5, # Falls back to ROS (not in mapping)
}, unit="rad")
If Tinkerforge is unavailable (not installed, connection failed, or fewer than 3 servo bricklets found), all motor commands fall back to ROS automatically.
Complete Example¶
from pib3 import Robot
import time
import math
def main():
with Robot(host="172.26.34.149") as robot:
print(f"Low-latency available: {robot.low_latency_available}")
# Smooth sinusoidal motion at 20 Hz
start_time = time.time()
duration = 5.0 # seconds
frequency = 20 # Hz
while time.time() - start_time < duration:
t = time.time() - start_time
# Sinusoidal motion between 0.2 and 0.8 radians
position = 0.5 + 0.3 * math.sin(2 * math.pi * 0.5 * t)
robot.set_joint("elbow_left", position, unit="rad")
time.sleep(1.0 / frequency)
print("Done!")
if __name__ == "__main__":
main()
Troubleshooting¶
| Problem | Cause | Solution |
|---|---|---|
low_latency_available is False |
Tinkerforge not installed | pip install tinkerforge |
| Connection failed | Check robot IP and port 4223 is accessible | |
| Fewer than 3 servo bricklets | Check wiring; or provide manual motor_mapping |
|
| Motors don't move | Servo power relay off | Check relay is enabled |
| Wrong bricklet UID | Use discover_servo_bricklets() to verify |
|
get_joint() returns old value |
sync_to_ros=False |
Set sync_to_ros=True in config |
| Jerky motion | Velocity too low | Increase velocity in configure_servo_channel() |
When to Use Each Mode
Use ROS mode (motor_mode="ros") for trajectory execution (run_trajectory()) where timing is managed by waypoints. Use direct mode (default) for interactive applications where you need immediate response to input.