A general-purpose PID controller for the Beam Bots 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.
- 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
Add bb_pid_controller to your list of dependencies in mix.exs:
def deps do
[
{:bb_pid_controller, "~> 0.2.0"}
]
end- BB framework (
~> 0.13)
Define a PID controller in your robot DSL:
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
endSend position setpoints to the PID controller via pubsub:
{: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.
| 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 |
| 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 |
| 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 |
| 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 |
| Option | Type | Default | Description |
|---|---|---|---|
rate |
pos_integer | 100 | Control loop frequency (Hz) |
The *_path options support atoms (field names) and integers (list indices):
# 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].valueSetpoint Topic ─────────────────────────┐
(configurable message/field) │
▼
┌─────────────────┐
Measurement Topic ────────────►│ BB.PID.Controller│
(configurable message/field) │ │
│ PIDControl │
│ .step() │
└────────┬─────────┘
│
▼
Output Topic
(configurable message/field)
- Controller subscribes to
setpoint_topicandmeasurement_topicat init - When a message arrives on
setpoint_topicwith matchingsetpoint_messagetype, the setpoint value is extracted and stored - When a message arrives on
measurement_topicwith matchingmeasurement_messagetype, the measurement value is extracted and stored - Every
1000/ratems, 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
- PID step is computed:
The controller validates configuration at init time:
- Unique sources -
{setpoint_topic, setpoint_message}must differ from{measurement_topic, measurement_message} - Non-empty paths -
setpoint_pathandmeasurement_pathcannot be empty - Valid output field -
output_fieldmust exist inoutput_messageschema and be numeric
Use encoder feedback to control position, outputting velocity commands:
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
}Use velocity feedback to control velocity, outputting effort commands:
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
}Full documentation is available at HexDocs.
