This tutorial shows you how to define a servo-controlled joint in your BB robot.
Prerequisites
- Completed Getting Started
- Servo wired to GPIO pin 17
Defining a Robot with a Servo Joint
Create a robot module with a revolute joint controlled by a servo:
defmodule MyRobot do
use BB
# A robot starts disarmed and won't move until armed. Arming is a command, so
# the robot has to declare one.
commands do
command :arm do
handler BB.Command.Arm
allowed_states [:disarmed]
end
command :disarm do
handler BB.Command.Disarm
allowed_states [:idle]
end
end
topology do
link :base do
joint :pan do
type :revolute
# Define the joint's motion limits
limit lower: ~u(-90 degree),
upper: ~u(90 degree),
velocity: ~u(60 degree_per_second),
effort: ~u(1 newton_meter)
# Attach the servo actuator
actuator :servo, {BB.Servo.Pigpio.Actuator, pin: 17}
# The servo reports nothing back, so estimate where it is
sensor :feedback, {BB.Sensor.OpenLoopPositionEstimator, actuator: :servo}
link :head do
# Child links go here
end
end
end
end
endThe sensor entry is the one part that isn't about hardware. BB.Robot.State is
written from BB.Message.Sensor.JointState messages and from nothing else, and an
RC servo has no return path, so without
BB.Sensor.OpenLoopPositionEstimator the joint reads as parked at its initial
position however far the servo travels. BB warns at compile time about a joint
nothing reports on.
Position Feedback covers how the estimate is produced.
Understanding the Configuration
Joint Limits
The limit block defines the physical constraints of your joint:
lower- Minimum position (maps to servo's minimum pulse)upper- Maximum position (maps to servo's maximum pulse)velocity- Maximum rotation speed (used for timing calculations)
These values are used by the actuator to:
- Map positions to PWM pulse widths
- Clamp commanded positions to safe values
- Calculate expected movement duration
Actuator Options
The actuator accepts these options:
actuator :servo, {BB.Servo.Pigpio.Actuator,
pin: 17, # Required: GPIO pin number
min_pulse: 500, # Optional: minimum pulse width in µs (default: 500)
max_pulse: 2500, # Optional: maximum pulse width in µs (default: 2500)
update_speed: ~u(50 hertz) # Optional: PWM update frequency (default: 50 Hz)
}Most servos work well with the defaults. Adjust min_pulse and max_pulse if
your servo has different endpoints.
Starting the Robot
Start your robot in your application supervision tree:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyRobot
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endOr start it manually in IEx:
iex> MyRobot.start_link()
{:ok, #PID<0.123.0>}Arming the Robot
A robot starts :disarmed and refuses every command until armed — the
framework drops them before they reach the driver, so a disarmed servo simply
won't move. Arm it through the command declared above:
iex> {:ok, cmd} = MyRobot.arm()
iex> BB.Command.await(cmd)
{:ok, :armed, [next_state: :idle]}Arming runs whatever prearm checks the robot defines, which is why it's a
command rather than a flag. Don't reach for BB.Safety directly — that skips
them.
Commanding the Servo
Send position commands to the actuator. An actuator can be addressed by its
unique name, as below, or by its full path through the topology
([:base, :pan, :servo] here):
# Move to centre (0 degrees)
:ok = BB.Actuator.set_position(MyRobot, :servo, 0.0)
# Move to -45 degrees (in radians)
:ok = BB.Actuator.set_position(MyRobot, :servo, -0.785)
# Using the unit sigil for degrees
import BB.Unit
:ok = BB.Actuator.set_position(MyRobot, :servo, ~u(-45 degree) |> BB.Robot.Units.to_radians())Note: BB uses radians internally. Convert degrees to radians when sending commands, or use the unit conversion functions.
set_position/4 publishes the command for observers and waits for the actuator
to take it, so it answers :ok or {:error, reason} — a refusal is something
you find out about rather than assume away:
case BB.Actuator.set_position(MyRobot, :servo, -0.785) do
:ok -> :moving
{:error, reason} -> Logger.error(Exception.message(reason))
endFor a control path that can't afford the round trip, delivery: :direct casts to
the actuator and publishes nothing. It always returns :ok, so a refusal reaches
only the log and telemetry:
BB.Actuator.set_position(MyRobot, :servo, -0.785, delivery: :direct)Position Clamping
The actuator automatically clamps positions to the joint limits:
# Joint limits are -90° to +90°
# This command will be clamped to +90° (π/2 radians)
BB.Actuator.set_position(MyRobot, :servo, 3.14) # Requested: 180°, actual: 90°Reversing Direction
If your servo rotates in the opposite direction to what you expect, reverse the actuator's joint transmission:
actuator :servo, {BB.Servo.Pigpio.Actuator, pin: 17} do
transmission do
reversed? true
end
endBB applies the transmission before passing motor-space positions to the Pigpio actuator, so direction reversal does not belong in the actuator options.
Example: Pan-Tilt Head
Here's a complete example with two servos for a pan-tilt mechanism:
defmodule PanTiltRobot do
use BB
commands do
command :arm do
handler BB.Command.Arm
allowed_states [:disarmed]
end
command :disarm do
handler BB.Command.Disarm
allowed_states [:idle]
end
end
topology do
link :base do
joint :pan do
type :revolute
limit lower: ~u(-90 degree), upper: ~u(90 degree),
velocity: ~u(90 degree_per_second), effort: ~u(1 newton_meter)
# Component names are unique across the whole robot, so each servo and
# estimator needs its own name — not `:servo` twice.
actuator :pan_servo, {BB.Servo.Pigpio.Actuator, pin: 17}
sensor :pan_feedback, {BB.Sensor.OpenLoopPositionEstimator, actuator: :pan_servo}
link :pan_platform do
joint :tilt do
type :revolute
limit lower: ~u(-45 degree), upper: ~u(45 degree),
velocity: ~u(60 degree_per_second), effort: ~u(1 newton_meter)
actuator :tilt_servo, {BB.Servo.Pigpio.Actuator, pin: 18}
sensor :tilt_feedback, {BB.Sensor.OpenLoopPositionEstimator, actuator: :tilt_servo}
link :camera_mount do
# Camera attached here
end
end
end
end
end
end
endCommand both servos:
{:ok, cmd} = PanTiltRobot.arm()
{:ok, :armed, _} = BB.Command.await(cmd)
# Look left and up — these name the actuators, not the joints
:ok = BB.Actuator.set_position(PanTiltRobot, :pan_servo, -0.785) # -45°
:ok = BB.Actuator.set_position(PanTiltRobot, :tilt_servo, 0.524) # +30°Next Steps
To get position feedback from your servos, see Position Feedback.