<!--
SPDX-FileCopyrightText: 2026 James Harton

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

# BB.Servo.Robotis Usage Rules

`bb_servo_robotis` drives Robotis/Dynamixel servos (Protocol 2.0, over a serial
U2D2 adapter) for [Beam Bots](https://hexdocs.pm/bb). It supplies three
components: `BB.Servo.Robotis.Controller` (a `BB.Controller` bus manager),
`BB.Servo.Robotis.Actuator` (a `BB.Actuator`, one per servo), and
`BB.Servo.Robotis.Bridge` (a `BB.Bridge` exposing the servo control table). For
BB framework basics, see `bb`'s rules (`mix usage_rules.sync <file> bb:all`);
this file covers only what's Robotis-specific.

## Core principles

1. **One controller per serial bus, many actuators.** The controller owns the
   serial connection and a shared ETS table; each actuator writes its
   `goal_position` there, and the controller's tick loop batches writes and
   bulk-reads positions (`fast_sync_read`). Every actuator names its controller
   and carries a unique `servo_id` (1–253) on that bus.
2. **Torque and disarm live on the controller, not the actuator.** The
   controller registers with `BB.Safety`, enables torque on arm, and disables
   it on disarm/crash. The actuator's `disarm/1` is a deliberate no-op.
3. **No separate position sensor.** Dynamixels report their own position; the
   controller polls and publishes `BB.Message.Sensor.JointState`. Do not add a
   sensor for feedback — there is no `BB.Servo.Robotis.Sensor`.

## Wiring it in

Declare the controller (and, optionally, the parameter bridge), then attach an
actuator to each joint:

```elixir
defmodule MyRobot.Robot do
  use BB

  controllers do
    controller :dynamixel,
      {BB.Servo.Robotis.Controller,
       port: "/dev/ttyUSB0",
       baud_rate: 1_000_000,
       control_table: Robotis.ControlTable.XM430}
  end

  parameters do
    bridge :robotis, {BB.Servo.Robotis.Bridge, controller: :dynamixel}
  end

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

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

        actuator :servo,
          {BB.Servo.Robotis.Actuator, servo_id: 1, controller: :dynamixel}

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

The actuator derives its motion range from the joint's `limit` — you never set
servo range or speed on the actuator itself.

## Options

**Controller** (`BB.Servo.Robotis.Controller`):

| Option | Default | Meaning |
|---|---|---|
| `:port` | required | Serial port path, e.g. `"/dev/ttyUSB0"` |
| `:baud_rate` | `1_000_000` | Bus baud rate |
| `:control_table` | `Robotis.ControlTable.XM430` | Must match the servo family (XM430 / XL330 / XL320) |
| `:loop_interval_ms` | `10` | Control-loop period (≈100 Hz) |
| `:status_poll_interval_ms` | `1000` | Temp/voltage/current/error poll; `0` disables |
| `:disarm_action` | `:disable_torque` | `:disable_torque`, or `:hold` to keep position |

**Actuator** (`BB.Servo.Robotis.Actuator`): `:servo_id` (1–253, required),
`:controller` (the controller's DSL name, required), `:position_deadband`
(default `2`, raw units — filters feedback noise), `:mode` (default `:position`;
also `:velocity`, `:current`, `:current_position`), `:expiry_action` (default
`:stop`; `:hold` to stay under power when a command's `duration` runs out).

**Bridge** (`BB.Servo.Robotis.Bridge`): `:controller` (required). Parameters are
addressed as `"servo_id:param_name"`, e.g.
`BB.Parameter.get_remote(MyRobot.Robot, :robotis, "1:position_p_gain")`.

## Commanding motion

Commands are issued in joint-space; BB applies the joint `transmission` so the
actuator receives motor-space values. Arm the robot first — a disarmed robot
ignores motion commands:

```elixir
# Either the actuator's unique name or its full path — `[:base, :shoulder, :servo]`
# here. A partial path matches no subscriber and the command goes nowhere.
BB.Actuator.set_position(MyRobot.Robot, :servo, 0.5)
```

What the actuator accepts depends on its `:mode` — `Position`/`Hold`/`Stop` in
`:position`, `Velocity`/`Hold`/`Stop` in `:velocity`, `Effort`/`Stop` in `:current`,
and all of them bar `Velocity` in `:current_position`. Anything else is refused with
`BB.Error.State.UnsupportedCommand`. Trajectory isn't implemented in any mode.

The mode is fixed at startup because changing it resets the servo's PID gains,
profile velocity and goal current. It's also checked against the model the servo
reports, so `mode: :current` on an XL430 — which has no current control — fails to
start rather than silently doing nothing.

`Stop` cuts torque, so the joint goes passive and can be backdriven — under load it
will sag. `Hold` puts it back under power at wherever it has come to rest, and a
`Position` command resumes on the way past, so you don't have to pair them:

```elixir
BB.Actuator.stop(MyRobot.Robot, :servo)
BB.Actuator.set_position(MyRobot.Robot, :servo, 0.5)  # re-applies torque itself
```

## Anti-patterns

- **Don't add a `sensor` for position feedback.** The controller publishes
  `JointState` under `[:sensor, <controller_name>, <joint>]`; there is no
  `BB.Servo.Robotis.Sensor` module.
- **Don't assume it runs under simulation.** Both the controller and the bridge
  default to `simulation: :omit`, so neither starts in simulation. Set
  `simulation: :mock` (or `:start`) if you need them there.
- **Don't use the bridge to re-enable torque.** Writing `"1:torque_enable"` back to
  `true` doesn't set a goal first, so the servo lunges for whatever goal it was
  chasing when torque was cut — from wherever it has drifted to since. `Hold` and
  `Position` write the goal and wait for it to land before torque comes on.
- **Don't use the bridge to change `operating_mode`.** The driver sets it at startup
  and assumes it stays put; changing it underneath also wipes the PID gains, profile
  velocity and goal current. Set `:mode` on the actuator instead.
- **Don't set the joint's velocity limit and expect the servo to ignore it.** The
  driver writes it to `profile_velocity`, so position moves now travel at that speed
  rather than flat out.
- **Don't reach for `Stop` to make the hardware safe.** It leaves the robot armed and
  commandable, and a passive joint under load falls rather than stopping. Disarm for
  that; the actuator's own `disarm/1` is a no-op because the controller owns the bus.
- **Don't share a `servo_id` or a serial `port`.** One controller per U2D2
  adapter, and each servo on the bus needs a distinct ID.

## Further reading

- [bb_servo_robotis docs](https://hexdocs.pm/bb_servo_robotis)
- `bb`'s actuator and safety rules (`bb:actuators`, `bb:safety-and-commands`)
  and [Writing an Actuator](https://hexdocs.pm/bb/12-writing-an-actuator.html)
