<!--
SPDX-FileCopyrightText: 2026 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 PID Controller
[](https://github.com/beam-bots/bb_pid_controller/actions/workflows/ci.yml)
[](https://opensource.org/licenses/Apache-2.0)
[](https://hex.pm/packages/bb_pid_controller)
[](https://api.reuse.software/info/github.com/beam-bots/bb_pid_controller)
A general-purpose PID controller for the [Beam Bots](https://github.com/beam-bots/bb) robotics framework.
This library provides a `BB.Controller` implementation that subscribes to configurable topics for setpoint and measurement values, runs a periodic PID loop, and publishes output to a configurable topic. One controller instance = one PID loop.
## Features
- **Configurable subscriptions** - subscribe to any topic/message combination for setpoint and measurement
- **Flexible path extraction** - extract values from nested message fields or list indices
- **Configurable output** - publish to any topic with any numeric message field
- **Runtime parameter updates** - PID gains can be changed at runtime via BB parameters
- **Validation** - validates configuration at init time with clear error messages
## Installation
Add `bb_pid_controller` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:bb_pid_controller, "~> 0.2.0"}
]
end
```
## Requirements
- BB framework (`~> 0.13`)
## Usage
Define a PID controller in your robot DSL:
```elixir
defmodule MyRobot do
use BB
controller :shoulder_pid, {BB.PID.Controller,
kp: 2.0, ki: 0.1, kd: 0.05,
output_min: -1.0, output_max: 1.0,
# Subscribe to position commands
setpoint_topic: [:actuator, :base_link, :shoulder, :pid],
setpoint_message: BB.Message.Actuator.Command.Position,
setpoint_path: [:position],
# Subscribe to encoder feedback
measurement_topic: [:sensor, :base_link, :shoulder, :encoder],
measurement_message: BB.Message.Sensor.JointState,
measurement_path: [:positions, 0],
# Publish velocity commands to servo
output_topic: [:actuator, :base_link, :shoulder, :servo],
output_message: BB.Message.Actuator.Command.Velocity,
output_field: :velocity,
output_frame_id: :shoulder,
rate: 100
}
topology do
link :base_link do
joint :shoulder, type: :revolute do
link :upper_arm do
end
end
end
end
end
```
## Sending Setpoints
Send position setpoints to the PID controller via pubsub:
```elixir
{:ok, msg} = BB.Message.new(
BB.Message.Actuator.Command.Position,
:shoulder,
position: 1.57
)
BB.PubSub.publish(MyRobot, [:actuator, :base_link, :shoulder, :pid], msg)
```
The controller will compute the PID output and publish velocity commands to the servo.
## Configuration Options
### PID Gains
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `kp` | float | required | Proportional gain |
| `ki` | float | 0.0 | Integral gain |
| `kd` | float | 0.0 | Derivative gain |
| `tau` | float | 1.0 | Derivative low-pass filter (0-1, 1=no filter) |
| `output_min` | float | -1.0 | Output clamp minimum |
| `output_max` | float | 1.0 | Output clamp maximum |
### Setpoint Subscription
| Option | Type | Description |
|--------|------|-------------|
| `setpoint_topic` | `[atom]` | Topic path to subscribe to |
| `setpoint_message` | module | Message module to filter for |
| `setpoint_path` | `[atom \| integer]` | Path to value in payload |
### Measurement Subscription
| Option | Type | Description |
|--------|------|-------------|
| `measurement_topic` | `[atom]` | Topic path to subscribe to |
| `measurement_message` | module | Message module to filter for |
| `measurement_path` | `[atom \| integer]` | Path to value in payload |
### Output Publication
| Option | Type | Description |
|--------|------|-------------|
| `output_topic` | `[atom]` | Topic path to publish to |
| `output_message` | module | Message module to construct |
| `output_field` | atom | Field name for output value |
| `output_frame_id` | atom | frame_id for constructed messages |
### Control Loop
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `rate` | pos_integer | 100 | Control loop frequency (Hz) |
## Path Extraction
The `*_path` options support atoms (field names) and integers (list indices):
```elixir
# Simple field access
setpoint_path: [:position] # payload.position
# List index access
measurement_path: [:positions, 0] # payload.positions |> Enum.at(0)
# Nested access
path: [:data, :readings, 0, :value] # payload.data.readings[0].value
```
## How It Works
### Architecture
```
Setpoint Topic ─────────────────────────┐
(configurable message/field) │
▼
┌─────────────────┐
Measurement Topic ────────────►│ BB.PID.Controller│
(configurable message/field) │ │
│ PIDControl │
│ .step() │
└────────┬─────────┘
│
▼
Output Topic
(configurable message/field)
```
### Message Flow
1. Controller subscribes to `setpoint_topic` and `measurement_topic` at init
2. When a message arrives on `setpoint_topic` with matching `setpoint_message` type, the setpoint value is extracted and stored
3. When a message arrives on `measurement_topic` with matching `measurement_message` type, the measurement value is extracted and stored
4. Every `1000/rate` ms, if both setpoint and measurement exist:
- PID step is computed: `output = Kp*error + Ki*integral + Kd*derivative`
- Output is clamped to `[output_min, output_max]`
- Output message is constructed and published to `output_topic`
### Validation
The controller validates configuration at init time:
1. **Unique sources** - `{setpoint_topic, setpoint_message}` must differ from `{measurement_topic, measurement_message}`
2. **Non-empty paths** - `setpoint_path` and `measurement_path` cannot be empty
3. **Valid output field** - `output_field` must exist in `output_message` schema and be numeric
## Common Use Cases
### Position Control with Velocity Output
Use encoder feedback to control position, outputting velocity commands:
```elixir
controller :joint_pid, {BB.PID.Controller,
kp: 5.0, ki: 0.5, kd: 0.1,
setpoint_topic: [:actuator, :joint, :pid],
setpoint_message: BB.Message.Actuator.Command.Position,
setpoint_path: [:position],
measurement_topic: [:sensor, :joint, :encoder],
measurement_message: BB.Message.Sensor.JointState,
measurement_path: [:positions, 0],
output_topic: [:actuator, :joint, :motor],
output_message: BB.Message.Actuator.Command.Velocity,
output_field: :velocity,
output_frame_id: :joint
}
```
### Velocity Control with Effort Output
Use velocity feedback to control velocity, outputting effort commands:
```elixir
controller :velocity_pid, {BB.PID.Controller,
kp: 1.0, ki: 0.1,
setpoint_topic: [:actuator, :wheel, :velocity_cmd],
setpoint_message: BB.Message.Actuator.Command.Velocity,
setpoint_path: [:velocity],
measurement_topic: [:sensor, :wheel, :encoder],
measurement_message: BB.Message.Sensor.JointState,
measurement_path: [:velocities, 0],
output_topic: [:actuator, :wheel, :motor],
output_message: BB.Message.Actuator.Command.Effort,
output_field: :effort,
output_frame_id: :wheel
}
```
## Documentation
Full documentation is available at [HexDocs](https://hexdocs.pm/bb_pid_controller).