Beam Bots Logo

Beam Bots Feetech/Waveshare servo control

CILicense: Apache 2.0Hex version badgeHexdocs badgeREUSE statusAsk DeepWiki

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

Supported Servos

Currently supports the following servo families via USB-to-TTL adapters:

Installation

Add bb_servo_feetech to your list of dependencies in mix.exs:

def deps do
[
{:bb_servo_feetech, "~> 0.6.0"}
]
end

Requirements

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.

modecommands
:position (default)Position, Trajectory, Effort, Hold, Stop
:velocityVelocity, 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:

Options:

OptionTypeDefaultDescription
portstringrequiredSerial port path (e.g., "/dev/ttyUSB0")
baud_rateinteger1000000Baud rate in bps
control_tablemoduleFeetech.ControlTable.STS3215Servo control table
loop_interval_msinteger10Control loop interval (100 Hz default)
status_poll_interval_msinteger1000Status polling interval (0 to disable)
disarm_actionatom:disable_torqueAction on disarm (:disable_torque or :hold)

Actuator

BB.Servo.Feetech.Actuator controls a single servo on the bus.

Options:

OptionTypeDefaultDescription
servo_id1-253requiredFeetech servo ID
controlleratomrequiredName of the controller in robot registry
position_deadbandinteger2Minimum position change (raw units) to publish feedback
mode:position | :velocity:positionOperating mode, fixed at startup
stall_torque~u() torque | nilfrom the modelRated stall torque, for scaling effort
expiry_action:stop | :hold:stopWhat 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:

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:

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:

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:

Safety

The controller implements BB.Safety:

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:

  1. Actuator clamps position to joint limits
  2. Converts angle to servo position (0-4095 units)
  3. Sends goal_position to controller
  4. Controller writes to servo via Feetech library
  5. Publishes BB.Message.Actuator.BeginMotion with:
    • initial_position - where the servo was
    • target_position - where it's going
    • expected_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:

FeatureFeetechRobotis
Position resolution4096 steps4096 steps
Load reportingpresent_load (percentage)present_current (mA)
Status byte bit 4Torque enabled (not an error)Hardware error
Sync readReturns list of valuesCallback-based
Operating modesPosition, velocityPosition, velocity, current, current-position
EffortA ceiling on a move (torque_limit)A torque goal (goal_current)
Mode switchingEEPROM write, finite budgetResets PID gains and profile settings
TrajectoriesSupportedNot implemented

Documentation

Full documentation is available at HexDocs.