Current section
Files
Jump to
Current section
Files
bb_servo_pigpio
README.md
README.md
<!--
SPDX-FileCopyrightText: 2025 James Harton
SPDX-License-Identifier: Apache-2.0
-->
<img src="https://github.com/beam-bots/bb/blob/main/logos/beam_bots_logo.png?raw=true" alt="Beam Bots Logo" width="250" />
# Beam Bots Pigpio servo control
[](https://github.com/beam-bots/bb_servo_pigpio/actions/workflows/ci.yml)
[](https://opensource.org/licenses/Apache-2.0)
[](https://hex.pm/packages/bb_servo_pigpio)
[](https://hexdocs.pm/bb_servo_pigpio)
[](https://api.reuse.software/info/github.com/beam-bots/bb_servo_pigpio)
[](https://deepwiki.com/beam-bots/bb_servo_pigpio)
# BB.Servo.Pigpio
BB integration for driving RC servos via pigpio on Raspberry Pi.
This library provides an actuator module for controlling RC servos directly
connected to Raspberry Pi GPIO pins using the pigpio daemon.
## Installation
Add `bb_servo_pigpio` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:bb_servo_pigpio, "~> 0.9.0"}
]
end
```
## Requirements
- Raspberry Pi with pigpio daemon running (`sudo pigpiod`)
- BB framework (`~> 0.25`)
## Usage
Define a joint with a servo actuator in your robot DSL:
```elixir
defmodule MyRobot do
use BB
# A robot won't move until armed, and arming is a command.
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 :shoulder do
type :revolute
limit lower: ~u(-45 degree), upper: ~u(45 degree),
velocity: ~u(60 degree_per_second), effort: ~u(1 newton_meter)
actuator :servo, {BB.Servo.Pigpio.Actuator, pin: 17}
sensor :feedback, {BB.Sensor.OpenLoopPositionEstimator, actuator: :servo}
link :arm do
# ...
end
end
end
end
end
```
The actuator automatically derives its configuration from the joint limits - no
need to specify servo rotation range or speed separately. The `sensor` entry is
not optional decoration: an RC servo reports nothing back, and without the
estimator nothing tells `BB.Robot.State` where the joint is.
## Sending Commands
Use the `BB.Actuator` module to send commands to servos. Arm the robot first —
commands to a disarmed robot are refused before they reach the driver:
```elixir
{:ok, cmd} = MyRobot.arm()
{:ok, :armed, _} = BB.Command.await(cmd)
```
`BB.Actuator.set_position/4` has two deliveries, chosen with `:delivery`. They
differ in transport, not in what the driver sees: both arrive at the actuator's
`handle_command/2`, and neither can skip the framework's arm check or its
joint-to-motor transmission.
Every function takes either the actuator's unique name or its full path
through the topology.
### Default Delivery (published and acknowledged)
The command is published to `[:actuator | path]`, which is what enables logging,
replay and multi-subscriber patterns, and delivered to the actuator by a call, so
the caller learns whether the joint is actually moving:
```elixir
case BB.Actuator.set_position(MyRobot, [:base, :shoulder, :servo], 0.5) do
:ok -> :moving
{:error, reason} -> handle_error(reason)
end
# With options
:ok = BB.Actuator.set_position(MyRobot, [:base, :shoulder, :servo], 0.5,
command_id: make_ref()
)
```
### Direct Delivery (for time-critical control)
Casts to the actuator and publishes nothing, for when responsiveness matters more
than observability. It **always returns `:ok`**, so a refusal reaches the log and
`[:bb, :actuator, :rejected]` telemetry and nowhere else — don't write an error
branch that can never run:
```elixir
# Fire-and-forget
BB.Actuator.set_position(MyRobot, :servo, 0.5, delivery: :direct)
```
## Components
### Actuator
`BB.Servo.Pigpio.Actuator` controls servo position via PWM.
**Options:**
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `pin` | integer | required | GPIO pin number |
| `min_pulse` | integer | 500 | Minimum PWM pulse width (microseconds) |
| `max_pulse` | integer | 2500 | Maximum PWM pulse width (microseconds) |
| `update_speed` | unit | 50 Hz | PWM update frequency |
To reverse the servo relative to the joint, configure the actuator's joint
transmission rather than passing an actuator option:
```elixir
actuator :servo, {BB.Servo.Pigpio.Actuator, pin: 17} do
transmission do
reversed? true
end
end
```
**Behaviour:**
- Maps joint position limits directly to PWM range
- Clamps commanded positions to joint limits
- Publishes `BB.Message.Actuator.BeginMotion` after each command
- Calculates expected arrival time based on joint velocity limit
### Sensor
Use `BB.Sensor.OpenLoopPositionEstimator` from the BB core library for position
feedback. It subscribes to actuator `BeginMotion` messages, interpolates position
during movement, and publishes it as `BB.Message.Sensor.JointState`.
```elixir
sensor :feedback, {BB.Sensor.OpenLoopPositionEstimator, actuator: :servo}
```
Give every servo-driven joint one. `BB.Robot.State` is written from `JointState`
messages and from nothing else — commanding a joint doesn't move it in state — so
a joint without an estimator stays at its initial configuration forever, and
forward kinematics, the URDF visualisers and inverse kinematics all keep working
from a robot that never moved. BB warns at compile time about a joint nothing
reports on.
## How It Works
### Position Mapping
The actuator maps the joint's position limits to the servo's PWM range:
```
Joint lower limit -> min_pulse (500 microseconds)
Joint upper limit -> max_pulse (2500 microseconds)
Joint centre -> mid_pulse (1500 microseconds)
```
For a joint with limits `-45 degrees` to `+45 degrees`:
- `-45 degrees` maps to 500 microseconds
- `0 degrees` maps to 1500 microseconds
- `+45 degrees` maps to 2500 microseconds
### Position Feedback
Since RC servos don't provide position feedback, the open-loop position
estimator estimates position based on commanded targets and expected arrival
times:
1. Actuator sends command and publishes `BeginMotion` with expected arrival time
2. Sensor receives `BeginMotion` and interpolates position during movement
3. After arrival time, sensor reports the target position
4. Sensor publishes the estimate as `JointState`, which is what writes
`BB.Robot.State`
That last step is why the estimator is part of the wiring rather than an extra:
it is the only thing that tells the rest of the framework where an RC servo is.
### Motion Lifecycle
When a position command is processed:
1. Actuator clamps position to joint limits
2. Converts angle to PWM pulse width
3. Sends PWM command to pigpiod
4. Publishes `BB.Message.Actuator.BeginMotion` with:
- `initial_position` - where the servo was
- `target_position` - where it's going
- `expected_arrival` - when it should arrive (monotonic milliseconds)
- `command_id` - correlation ID (if provided)
- `command_type` - `:position`
## Documentation
Full documentation is available at [HexDocs](https://hexdocs.pm/bb_servo_pigpio).