Beam Bots Feetech/Waveshare servo control
BB.Servo.Feetech
BB integration for driving Feetech/WaveShare STS/SCS series serial bus servos.
This library provides controller, actuator, and parameter bridge modules for integrating Feetech servos with the Beam Bots robotics framework. Unlike PWM servos, Feetech serial bus servos provide closed-loop position feedback.
Features
- Closed-loop position feedback - Servos report their actual position
- Multiple servos on one bus - All servos share a single serial connection
- Position and velocity modes - Continuous rotation as well as positioning
- Stop and hold - Go passive on command, and come back under power without lunging
- Safety integration - Torque is automatically disabled when the robot is disarmed or crashes
- Status monitoring - Temperature, voltage, load, and hardware error reporting
- Parameter access - Read and write servo configuration via the BB parameter system
Supported Servos
Currently supports the following servo families via USB-to-TTL adapters:
- STS3215
- STS3032
- SCS series (using appropriate control table)
Installation
Add bb_servo_feetech to your list of dependencies in mix.exs:
def deps do
[
{:bb_servo_feetech, "~> 0.6.0"}
]
end
Requirements
- USB-to-TTL serial adapter compatible with Feetech servos
- Feetech STS/SCS series servos
- BB framework (
~> 0.25)
Usage
Define a controller and joints with servo actuators in your robot DSL:
defmodule MyRobot do
use BB
controllers do
controller :feetech, {BB.Servo.Feetech.Controller,
port: "/dev/ttyUSB0",
baud_rate: 1_000_000,
control_table: Feetech.ControlTable.STS3215
}
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 :shoulder_servo, {BB.Servo.Feetech.Actuator,
servo_id: 1,
controller: :feetech
}
link :upper_arm do
joint :elbow do
type :revolute
limit lower: ~u(-90 degree), upper: ~u(90 degree),
velocity: ~u(60 degree_per_second), effort: ~u(1 newton_meter)
actuator :elbow_servo, {BB.Servo.Feetech.Actuator,
servo_id: 2,
controller: :feetech
}
link :forearm do
end
end
end
end
end
end
end
The actuator automatically derives its configuration from the joint limits - no need to specify servo rotation range or speed separately. Position feedback is handled by the controller; no separate sensor is needed.
Sending Commands
Use the BB.Actuator module to send commands to servos. Arm the robot first —
commands to a disarmed robot are refused before they reach the driver:
{:ok, cmd} = MyRobot.arm()
{:ok, :armed, _} = BB.Command.await(cmd)
There are three ways to deliver a command. They differ in transport, not in
what the driver sees: all three arrive at the actuator's handle_command/2,
and none can skip the framework's arm check or its joint-to-motor transmission.
Every function takes either the actuator's unique name or its full path through the topology.
Pubsub Delivery (for orchestration)
Commands are published via pubsub, enabling logging, replay, and multi-subscriber patterns:
# By name
BB.Actuator.set_position(MyRobot, :shoulder_servo, 0.5)
# Or by full path
BB.Actuator.set_position(MyRobot, [:base, :shoulder, :shoulder_servo], 0.5)
# With options
BB.Actuator.set_position(MyRobot, :shoulder_servo, 0.5,
command_id: make_ref()
)
Direct Delivery (for time-critical control)
Commands bypass pubsub for lower latency:
BB.Actuator.set_position!(MyRobot, :shoulder_servo, 0.5)
Synchronous Delivery (with acknowledgement)
Wait for the actuator to acknowledge the command:
case BB.Actuator.set_position_sync(MyRobot, :shoulder_servo, 0.5) do
{:ok, :accepted} -> :ok
{:error, reason} -> handle_error(reason)
end
Stopping, Holding, and Other Modes
What an actuator accepts depends on its mode. Anything outside that list is
refused with BB.Error.State.UnsupportedCommand before it reaches the driver.
mode | commands |
|---|---|
:position (default) | Position, Trajectory, Effort, Hold, Stop |
:velocity | Velocity, Effort, Hold, Stop |
# Go passive — the joint can be backdriven by hand, and will sag under load
BB.Actuator.stop(MyRobot, :shoulder_servo)
# Back under power, holding wherever it came to rest
BB.Actuator.hold(MyRobot, :shoulder_servo)
# Only in an actuator configured `mode: :velocity`
BB.Actuator.set_velocity(MyRobot, :wheel_servo, 2.0, duration: 500)
stop/3 is not the safety path: it leaves the robot armed and commandable.
Making the hardware safe is MyRobot.disarm(), which is robot-wide.
A joint left passive by a Stop comes back under power on the next motion
command, at the position it came to rest rather than the goal it was chasing
when torque was cut — so callers don't have to pair stop with hold.
Effort is a Ceiling, Not a Goal
STS servos have no torque goal register. Command.Effort writes torque_limit,
which caps what a move may draw:
# Cap the gripper at 0.4 Nm, then close it — it squeezes to the limit
BB.Actuator.set_effort(MyRobot, :gripper_servo, 0.4)
BB.Actuator.set_position(MyRobot, :gripper_servo, 0.0)
Setting an effort on its own moves nothing. The newton-metre figure is scaled
against the servo's rated stall torque, which no register reports — it comes
from BB.Servo.Feetech.Model, keyed on the model number the servo reports.
That number is a rough guide. Every STS3215 variant reports 777, whether it is
the 7.4 V 1:345 (19.5 kgf·cm), the 7.4 V 1:191 (27.4 kgf·cm), or the 12 V 1:345
(30 kgf·cm). The table carries the first of those; set stall_torque: on the
actuator if you have one of the others. Stall torque also falls with supply
voltage, and the servo's own overload protection can wind the ceiling down under
sustained load.
Components
Controller
BB.Servo.Feetech.Controller manages the serial connection to the Feetech
bus. Define one controller per serial adapter. The controller handles:
- Serial communication with servos
- Position feedback polling for all registered servos
- Status monitoring (temperature, voltage, load, errors)
- Torque enable/disable on arm/disarm
Options:
| Option | Type | Default | Description |
|---|---|---|---|
port | string | required | Serial port path (e.g., "/dev/ttyUSB0") |
baud_rate | integer | 1000000 | Baud rate in bps |
control_table | module | Feetech.ControlTable.STS3215 | Servo control table |
loop_interval_ms | integer | 10 | Control loop interval (100 Hz default) |
status_poll_interval_ms | integer | 1000 | Status polling interval (0 to disable) |
disarm_action | atom | :disable_torque | Action on disarm (:disable_torque or :hold) |
Actuator
BB.Servo.Feetech.Actuator controls a single servo on the bus.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
servo_id | 1-253 | required | Feetech servo ID |
controller | atom | required | Name of the controller in robot registry |
position_deadband | integer | 2 | Minimum position change (raw units) to publish feedback |
mode | :position | :velocity | :position | Operating mode, fixed at startup |
stall_torque | ~u() torque | nil | from the model | Rated stall torque, for scaling effort |
expiry_action | :stop | :hold | :stop | What to do when a command's duration runs out |
Configure direction reversal on the joint transmission, not as an actuator option:
actuator :servo, {BB.Servo.Feetech.Actuator, servo_id: 1, controller: :feetech} do
transmission do
reversed? true
end
end
Behaviour:
- Maps joint position limits to servo position (0-4095 units for 360 degrees)
- Clamps commanded positions to joint limits
- Registers with controller for position feedback
- Publishes
BB.Message.Actuator.BeginMotionafter each command - Torque management is delegated to the controller
Position Feedback
Unlike PWM servos, Feetech servos report their actual position. The controller
polls all registered servos and publishes BB.Message.Sensor.JointState messages.
No separate sensor is needed in the robot definition.
Subscribe to position updates:
BB.subscribe(MyRobot, [:sensor, :feetech, :shoulder])
Status Monitoring
The controller periodically reads status registers and publishes
BB.Servo.Feetech.Message.ServoStatus messages containing:
temperature- Internal temperature in Celsiusvoltage- Input voltage in Voltsload- Load percentage (0-100)hardware_error- Hardware error flags (nil if no errors)
Hardware errors are also reported to the safety system.
Subscribe to status updates:
BB.subscribe(MyRobot, [:sensor, :feetech, :servo_status])
Parameter Bridge
BB.Servo.Feetech.Bridge exposes servo configuration through the BB parameter
system. Parameters are identified by strings in the format "servo_id:param_name".
defmodule MyRobot do
use BB
parameters do
bridge :feetech, {BB.Servo.Feetech.Bridge, controller: :feetech}
end
end
# Read parameter
{:ok, 50} = BB.Parameter.get_remote(MyRobot, :feetech, "1:position_p_gain")
# Write parameter (control params)
:ok = BB.Parameter.set_remote(MyRobot, :feetech, "1:position_p_gain", 100)
# List all parameters
{:ok, params} = BB.Parameter.list_remote(MyRobot, :feetech)
Parameter categories:
- info - Read-only identification (firmware_version, model_number)
- config - EEPROM settings, require torque off to write (limits, gains, mode)
- control - SRAM settings, writable at runtime (goal_position, torque_enable)
How It Works
Architecture
Controller (GenServer)
|
v wraps
Feetech (Serial communication)
^
| used by
Actuator (GenServer) --publishes--> BeginMotion
|
v registers with
Controller --publishes--> JointState (position feedback)
--publishes--> ServoStatus (status monitoring)
Multiple actuators share a single controller. Each actuator controls a servo with a unique ID (1-253) on the bus.
Position Mapping
The actuator maps the joint's position limits to the servo's position range (4096 steps per revolution, centre at 2048):
Joint centre -> 2048 (servo centre)
Joint lower limit -> 2048 - (range/2) steps
Joint upper limit -> 2048 + (range/2) steps
For a joint with limits -90 degrees to +90 degrees:
-90 degreesmaps to 1024 (90 degrees CCW from centre)0 degreesmaps to 2048 (servo centre)+90 degreesmaps to 3072 (90 degrees CW from centre)
Safety
The controller implements BB.Safety:
- On arm - Torque is enabled on all registered servos
- On disarm - Torque is disabled (or held, if configured)
- On crash - Torque is disabled without requiring GenServer state
Hardware errors detected during status polling are reported to the safety system and may trigger disarm depending on your safety configuration.
Motion Lifecycle
When a position command is processed:
- Actuator clamps position to joint limits
- Converts angle to servo position (0-4095 units)
- Sends goal_position to controller
- Controller writes to servo via Feetech library
- Publishes
BB.Message.Actuator.BeginMotionwith:initial_position- where the servo wastarget_position- where it's goingexpected_arrival- when it should arrive (based on velocity limit)command_id- correlation ID (if provided)command_type-:position
Position feedback is handled separately by the controller's polling loop.
Differences from bb_servo_robotis
While this package follows the same architecture as bb_servo_robotis, there
are some differences in the underlying servo protocol:
| Feature | Feetech | Robotis |
|---|---|---|
| Position resolution | 4096 steps | 4096 steps |
| Load reporting | present_load (percentage) | present_current (mA) |
| Status byte bit 4 | Torque enabled (not an error) | Hardware error |
| Sync read | Returns list of values | Callback-based |
| Operating modes | Position, velocity | Position, velocity, current, current-position |
| Effort | A ceiling on a move (torque_limit) | A torque goal (goal_current) |
| Mode switching | EEPROM write, finite budget | Resets PID gains and profile settings |
| Trajectories | Supported | Not implemented |
Documentation
Full documentation is available at HexDocs.