<!--
SPDX-FileCopyrightText: 2026 James Harton

SPDX-License-Identifier: Apache-2.0
-->

# BB.Servo.PCA9685 Usage Rules

`bb_servo_pca9685` drives RC servos through a PCA9685 16-channel PWM board over
I²C for [Beam Bots](https://hexdocs.pm/bb). It supplies two DSL components: a
robot-level `BB.Servo.PCA9685.Controller` (a `BB.Controller` wrapping one
physical board) and a per-joint `BB.Servo.PCA9685.Actuator` (a `BB.Actuator`
driving one channel). For BB framework basics, see `bb`'s rules
(`mix usage_rules.sync <file> bb:all`); this file covers only what's specific to
this driver.

## Core principles

1. **One controller per board, many actuators per controller.** The controller
   owns the I²C connection; each actuator names it via `controller:` and claims
   one channel (`0..15`). A board has 16 channels — add a second controller (at
   a different `address:`) for more servos.
2. **Servo geometry comes from the joint, not the actuator.** The actuator reads
   the joint's `limit` (via the injected motor profile) and maps that motor
   range linearly onto the PWM pulse range. You never declare rotation range or
   speed on the actuator — only the pulse endpoints if the servo's defaults
   don't fit.
3. **Position feedback is open-loop, and not optional.** RC servos report
   nothing back, and this driver declares no `capabilities/1`, so pair every
   actuator with core's `BB.Sensor.OpenLoopPositionEstimator`, which
   interpolates position from the `BeginMotion` message the actuator publishes
   and reports it as `BB.Message.Sensor.JointState`. `BB.Robot.State` is written
   from those 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
   and every consumer of joint positions (forward kinematics, the URDF
   visualisers, IK, which seeds each solve from the current configuration) works
   from a robot that never moved. BB warns at compile time when it finds one.

## Installing

```sh
mix igniter.install bb_servo_pca9685
```

This adds a controller entry plus a `:config.:pca9685` param group to your robot
and sets the bus on the application child spec. Actuator and sensor entries are
per-joint and are **not** added automatically — the installer prints a snippet
to copy into each joint.

## Wiring it in

The controller goes at robot level; each servo is an actuator inside its joint,
with an open-loop estimator alongside it:

```elixir
defmodule MyRobot.Robot do
  use BB

  controllers do
    controller :pca9685, {BB.Servo.PCA9685.Controller, bus: "i2c-1", address: 0x40}
  end

  topology do
    link :base do
      joint :shoulder do
        type :revolute

        limit lower: ~u(-45 degree),
              upper: ~u(45 degree),
              effort: ~u(1 newton_meter),
              velocity: ~u(60 degree_per_second)

        actuator :shoulder_servo, {BB.Servo.PCA9685.Actuator, channel: 0, controller: :pca9685}

        sensor :shoulder_feedback,
               {BB.Sensor.OpenLoopPositionEstimator, actuator: :shoulder_servo}

        link :upper_arm do
        end
      end
    end
  end
end
```

Command it with `BB.Actuator` once the robot is armed (values are joint-space
radians; BB applies the joint transmission before the driver sees motor-space):

```elixir
# Published for observers and delivered by a call, so a refusal reaches you
:ok = BB.Actuator.set_position(MyRobot.Robot, :shoulder_servo, 0.5)

# Cast, for control paths that can't afford the round trip. Always returns `:ok`
BB.Actuator.set_position(MyRobot.Robot, :shoulder_servo, 0.5, delivery: :direct)
```

## Options

**Controller** (`controllers` slot):

| Option | Default | Meaning |
|---|---|---|
| `:bus` | required | I²C bus name, e.g. `"i2c-1"` |
| `:address` | required | I²C address, e.g. `0x40` |
| `:pwm_freq` | `50` | PWM frequency in Hz (50 suits most analogue servos) |
| `:oe_pin` | none | GPIO pin for hardware output-enable |

**Actuator** (per-joint `actuator` slot):

| Option | Default | Meaning |
|---|---|---|
| `:channel` | required | Board channel, `0..15` |
| `:controller` | required | Name of the controller entry to route through |
| `:min_pulse` | `500` | Pulse width (µs) mapped to the joint's lower limit |
| `:max_pulse` | `2500` | Pulse width (µs) mapped to the joint's upper limit |

## Anti-patterns

- **Don't try to set the servo's travel range or speed on the actuator.** There
  is no such option — it derives everything from the joint `limit`. Widen the
  joint limit or adjust `:min_pulse`/`:max_pulse` to match the physical servo.
- **Don't lean on `disarm/1` as a power cut if the controller might be dead.**
  Both the actuator's disarm (pulse → 0) and the controller's disarm (OE pin
  high) route through the live controller process; if it has crashed the disarm
  fails and the robot enters `:error`. For a kill that survives a dead
  controller, wire `:oe_pin` — a clean controller shutdown pulls it high in the
  device's `terminate/2`.
- **Don't expect the controller to run under simulation.** Controllers default
  to `simulation: :omit`, so the real PCA9685 controller does not start; set
  `simulation: :mock` or `:start` on the entry if you need it. Actuators are
  swapped for `BB.Sim.Actuator` and the open-loop estimator works unchanged.
- **Address the actuator by name or by full path, never a partial one.** Both
  deliveries accept either, and a name is resolved for you — naming an actuator
  the robot doesn't have raises. A partial path like `[:shoulder, :servo]` still
  reaches the servo, because only its last element addresses the process, but it
  publishes on a topic nothing is subscribed to, so observers silently miss the
  command.
- **Don't reuse a component name across joints.** Names are unique robot-wide,
  not scoped to their joint, so `:servo` on two joints fails to compile. Name
  them after the joint (`:shoulder_servo`, `:elbow_servo`).

## Further reading

- [bb_servo_pca9685 docs](https://hexdocs.pm/bb_servo_pca9685)
- `bb`'s actuator and safety rules (`bb:actuators`, `bb:safety-and-commands`)
