Beam Bots supports running robots in simulation mode, allowing you to develop and test robot behaviour without physical hardware. A single robot definition works for both hardware and simulation - you just change how you start it.
Prerequisites
Complete Your First Robot and Starting and Stopping first.
Starting in Simulation Mode
Your robot's child spec calls the generated robot_opts/0 (in application.ex), which boots it in kinematic simulation when the SIMULATE environment variable is set (see Starting and Stopping). So to run the whole app in simulation, set it when you launch:
SIMULATE=1 iex -S mix
The robot is now running entirely in software. Actuators receive commands and publish motion messages, but no hardware communication occurs.
Checking Simulation Mode
You can check whether a robot is running in simulation mode:
iex> BB.Robot.Runtime.simulation_mode(MyRobot.Robot)
:kinematic
# Hardware mode returns nil
iex> BB.Robot.Runtime.simulation_mode(MyRobot.Robot)
nilHow Simulation Works
In simulation mode:
- Actuators are replaced - Real actuator modules are swapped for
BB.Sim.Actuator - Controllers are omitted - By default, hardware controllers don't start
- Messages flow normally - Commands,
BeginMotion, andJointStatemessages work as usual - Safety system is active - You must still arm the robot before sending commands
The simulated actuator:
- Receives position commands via the normal API
- Calculates motion timing from joint velocity limits in your DSL
- Publishes
BeginMotionmessages with realistic timing - Clamps positions to joint limits
The existing OpenLoopPositionEstimator sensor works unchanged, estimating position from BeginMotion messages.
Example: Testing Motion
With your robot running in simulation (see Starting in Simulation Mode above):
# Arm the robot (required even in simulation)
:ok = BB.Safety.arm(MyRobot.Robot)
# Send a position command
BB.Actuator.set_position!(MyRobot.Robot, :shoulder_motor, 1.57)
# The OpenLoopPositionEstimator will estimate position over time
Process.sleep(500)
position = BB.Robot.Runtime.joint_position(MyRobot.Robot, :shoulder)Controller Behaviour in Simulation
By default, controllers are omitted in simulation mode. You can customise this per-controller using the simulation option in the DSL:
controllers do
# Won't start in simulation (default)
controller :pca9685, {BB.Servo.PCA9685.Controller, bus: "i2c-1"},
simulation: :omit
# Starts a mock controller that accepts but ignores commands
controller :dynamixel, {BB.Servo.Robotis.Controller, port: "/dev/ttyUSB0"},
simulation: :mock
# Starts the real controller (for external simulator integration)
controller :gazebo_bridge, {MyApp.GazeboBridge, url: "localhost:11345"},
simulation: :start
endThe three options are:
| Option | Behaviour |
|---|---|
:omit | Controller not started (default) |
:mock | Mock controller started - accepts commands but does nothing |
:start | Real controller started |
When to Use Each Option
:omit- Most hardware controllers (I2C, serial, GPIO). The simulated actuator doesn't need them.:mock- When actuators query the controller for state during initialisation.:start- For external simulator bridges (Gazebo, MuJoCo) that need to run in simulation.
Bridge Behaviour in Simulation
Parameter bridges also support the simulation option, with the same three modes:
parameters do
# Won't start in simulation (default)
bridge :mavlink, {BBMavLink.ParameterBridge, conn: "/dev/ttyACM0"},
simulation: :omit
# Starts a mock bridge that accepts but ignores operations
bridge :gcs, {MyApp.GCSBridge, url: "ws://gcs.local/socket"},
simulation: :mock
# Starts the real bridge (for external system integration)
bridge :phoenix, {BBPhoenix.ParameterBridge, url: "ws://localhost:4000/socket"},
simulation: :start
end| Option | Behaviour |
|---|---|
:omit | Bridge not started (default) |
:mock | Mock bridge started - accepts operations but does nothing |
:start | Real bridge started |
Kinematic Simulation
The :kinematic simulation mode provides position/velocity interpolation without physics:
- Positions are clamped to joint limits (
lower,upper) - Travel time is calculated from velocity limits
- No acceleration, inertia, or gravity simulation
This is sufficient for:
- Testing control logic and state machines
- Verifying command sequences
- UI development without hardware
- Integration testing
Future Simulation Modes
The simulation option is an atom to allow future expansion:
# Current: kinematic simulation
config :my_robot, MyRobot.Robot, simulation: :kinematic
# Future: external physics engine
config :my_robot, MyRobot.Robot, simulation: :external
# Future: built-in physics
config :my_robot, MyRobot.Robot, simulation: :physicsEnvironment-Based Mode Selection
The generated robot_opts/0 already switches on the SIMULATE environment variable, so SIMULATE=1 iex -S mix runs in simulation and a plain start runs on hardware.
To pin a mode per environment, set the robot's options in config — robot_opts/0 reads them when SIMULATE is unset:
# config/dev.exs
config :my_robot, MyRobot.Robot, simulation: :kinematic
# config/prod.exs (or target.exs for Nerves) — hardware is the defaultIf you need richer logic, edit robot_opts/0 in application.ex directly.
Testing with Simulation
Run your robot in simulation for integration tests by selecting the mode in config/test.exs:
# config/test.exs
config :my_robot, MyRobot.Robot, simulation: :kinematicYour application then starts the robot in simulation for the test run, so tests drive the already-running robot:
defmodule MyRobotTest do
use ExUnit.Case
test "robot moves to home position" do
:ok = BB.Safety.arm(MyRobot.Robot)
:ok = BB.Command.execute(MyRobot.Robot, :home)
# Verify the robot reached home position
assert_eventually(fn ->
pos = BB.Robot.Runtime.joint_position(MyRobot.Robot, :shoulder)
abs(pos - 0.0) < 0.01
end)
end
endSubscribing to Simulated Motion
You can subscribe to motion messages from simulated actuators:
# Subscribe to actuator messages
BB.PubSub.subscribe(MyRobot.Robot, [:actuator, :base_link, :shoulder, :motor])
# Send a command
BB.Actuator.set_position!(MyRobot.Robot, :motor, 1.0)
# Receive the BeginMotion message
receive do
{:bb, _path, %BB.Message{payload: %BB.Message.Actuator.BeginMotion{} = motion}} ->
IO.puts("Moving from #{motion.initial_position} to #{motion.target_position}")
IO.puts("Expected arrival: #{motion.expected_arrival}ms")
endLimitations
Kinematic simulation doesn't model:
- Physics (gravity, inertia, friction, collisions)
- Sensor noise or latency
- Hardware-specific behaviour
- External disturbances
For high-fidelity simulation, consider integrating with an external physics engine like Gazebo or MuJoCo using simulation: :start controllers.
What's Next?
You now know how to:
- Run robots in simulation mode
- Configure controller behaviour in simulation
- Use simulation for development and testing
For more advanced topics, see:
- Custom States and Command Categories - Define operational modes and concurrent commands
- Safety - Understanding the safety system
- Parameters - Runtime-adjustable configuration