Current section

Files

Jump to
bb_pid_controller usage-rules.md
Raw

usage-rules.md

<!--
SPDX-FileCopyrightText: 2026 James Harton
SPDX-License-Identifier: Apache-2.0
-->
# BB.PID.Controller Usage Rules
`bb_pid_controller` provides `BB.PID.Controller`, a general-purpose PID
implementation of the `BB.Controller` behaviour for
[Beam Bots](https://hexdocs.pm/bb). For BB framework basics, see `bb`'s rules
(`mix usage_rules.sync <file> bb:all`); this file covers only what's specific to
the controller.
## Core principles
1. **It is a declared, supervised controller — not a solver you pass.** Wire it
into the `controllers` block as `{BB.PID.Controller, opts}`. BB wraps it in a
supervised process; you never write a `child_spec`.
2. **It is entirely message-driven.** The controller subscribes to a setpoint
topic and a measurement topic, and publishes its output to a third topic. It
does *not* touch actuators, joints, or the topology directly — everything is
PubSub. One controller instance is exactly one PID loop; declare several for
several loops.
3. **The control law lives in `BB.PID.Kernel`, not in the controller.** It is an
`Nx.Defn` kernel written elementwise over its tensors, so it drives one loop
or a batch of them; the controller is the single-loop case. Reach for the
kernel directly if you need PID inside a larger Nx computation, or many loops
advanced in one call.
4. **It runs its own tick loop at `rate` Hz**, via `BB.Loop`. Each tick it
publishes only once *both* a setpoint and a measurement have arrived; until
then it stays quiet. The loop schedules against an absolute deadline and
drops whole missed periods rather than firing catch-up ticks, and reports
its achieved interval and skipped-period count on `[:bb, :loop, :tick]`.
5. **The integral and derivative terms use the measured elapsed time** between
steps, not the nominal `rate`. Gains are therefore per-second: `ki` is
integral gain per second of accumulated error, `kd` is derivative gain per
second of error change.
## Wiring it in
```elixir
controllers do
controller :shoulder_pid, {BB.PID.Controller,
kp: 2.0, ki: 0.1, kd: 0.05,
output_min: -1.0, output_max: 1.0,
setpoint_topic: [:actuator, :base_link, :shoulder, :pid],
setpoint_message: BB.Message.Actuator.Command.Position,
setpoint_path: [:position],
measurement_topic: [:sensor, :base_link, :shoulder, :encoder],
measurement_message: BB.Message.Sensor.JointState,
measurement_path: [:positions, 0],
output_topic: [:actuator, :base_link, :shoulder, :servo],
output_message: BB.Message.Actuator.Command.Velocity,
output_field: :velocity,
output_frame_id: :shoulder,
rate: 100}
end
```
`mix igniter.install bb_pid_controller` scaffolds this entry with `kp`/`ki`/`kd`
wired to tunable parameters and the topic paths left as `[:TODO]` to fill in.
## Options
| Option | Default | Meaning |
|---|---|---|
| `kp` | required | Proportional gain |
| `ki` | `0.0` | Integral gain |
| `kd` | `0.0` | Derivative gain |
| `tau` | `1.0` | Derivative low-pass filter (`0``1`, `1` = no filter) |
| `output_min` / `output_max` | `-1.0` / `1.0` | Output clamp bounds |
| `setpoint_topic` / `setpoint_message` / `setpoint_path` | required | Where the target comes from, the message type to match, and the path to the value in its payload |
| `measurement_topic` / `measurement_message` / `measurement_path` | required | Same three for the feedback signal |
| `output_topic` / `output_message` / `output_field` / `output_frame_id` | required | Topic to publish to, message type to build, numeric field for the output, and its `frame_id` |
| `rate` | `100` | Loop frequency in Hz. The loop self-corrects for drift; watch `:skipped` on `[:bb, :loop, :tick]` to see whether the machine is actually keeping up |
A `*_path` is a list of atoms (field names) and integers (list indices):
`[:position]`, `[:positions, 0]`, `[:data, :readings, 0, :value]`.
The gains are plain floats, but you can point them at robot parameters —
`kp: param([:config, :shoulder_pid, :kp])` — to make them tunable at runtime;
`handle_options/2` rebuilds the PID state when a parameter changes.
## Anti-patterns
- **Don't expect it to run in simulation by default.** DSL-declared controllers
default to `simulation: :omit`, so a robot started in simulation mode does
*not* start this controller. Add `simulation: :mock` or `simulation: :start`
to the entry if you need it under simulation.
- **Don't give the setpoint and measurement the same source.** Init fails if
`{setpoint_topic, setpoint_message}` equals `{measurement_topic,
measurement_message}`; paths must be non-empty; and `output_field` must name a
numeric field on `output_message` or init rejects the config.
- **Don't rely on it stopping when the robot disarms.** This controller does not
implement `disarm/1` and does not register with `BB.Safety` — it keeps ticking
and publishing regardless of safety state. If its output must cease on disarm,
that has to come from the downstream actuator, not from here.
## Further reading
- [bb_pid_controller docs](https://hexdocs.pm/bb_pid_controller)
- `bb`'s controller and safety rules (`bb:safety-and-commands`) and
[Reactive Controllers](https://hexdocs.pm/bb/reactive-controllers.html)