This file provides guidance to coding assistances while working with this project.
Beam Bots is a framework for building resilient robotics projects in Elixir. It provides a Spark DSL for defining robot topologies (links, joints, sensors, actuators) with automatic supervision tree generation that mirrors the physical structure for fault isolation.
See documentation/tutorials/ for guided tutorials:
01-first-robot.md- defining robots with the DSL02-starting-and-stopping.md- supervision trees and fault isolation03-sensors-and-pubsub.md- publishing and subscribing to messages04-kinematics.md- computing link positions with forward kinematics05-commands.md- the command system and robot state machine06-urdf-export.md- exporting to URDF for ROS tools07-parameters.md- runtime-adjustable configuration08-parameter-bridges.md- bidirectional parameter access with remote systems09-inverse-kinematics.md- solving inverse kinematics10-simulation.md- running robots in simulation mode
The DSL reference is in documentation/dsls/DSL-BB.md.
# Run all checks (formatter, tests, credo, dialyzer, etc.)
mix check --no-retry
# Run tests
mix test
mix test path/to/test_file.exs # Single file
mix test path/to/test_file.exs:42 # Single test at line
# Code quality
mix format
mix credo --strict
mix dialyzer
# Spark DSL tools
mix spark.formatter # Update formatter with DSL locals
mix spark.cheat_sheets # Generate DSL documentation
# URDF export
mix bb.to_urdf MyRobot # Print URDF to stdout
mix bb.to_urdf MyRobot -o robot.urdf # Write to fileThe core DSL defines robot structure using nested entities:
- settings - robot name, registry/supervisor modules
- topology - contains links, joints, sensors, actuators in a tree structure
- sensors - robot-level sensors (GPS, battery, etc.)
- controllers - robot-level controller processes
- commands - commands with handlers and state machine integration
Within the topology:
- link - kinematic link (solid body) with visual, collision, inertial properties
- joint - connection between links (revolute, prismatic, fixed, continuous, floating, planar)
- sensor/actuator - child processes attached to links or joints
The DSL supports physical units via ~u sigil (e.g., ~u(0.1 meter), ~u(90 degree)).
Transformers run in sequence to process DSL at compile-time:
DefaultNameTransformer- sets robot name to module name if unsetTopologyTransformer- validates link hierarchySupervisorTransformer- generates supervision tree specsRobotTransformer- builds optimisedBB.Robotstruct, injectsrobot/0function
Robot struct (lib/bb/robot.ex): Optimised representation with:
- Flat maps for O(1) lookup of links/joints/sensors/actuators
- All units converted to SI base (metres, radians, kg)
- Pre-computed topology for traversal
Supervision tree (lib/bb/supervisor.ex): Mirrors robot topology for fault isolation. Crashes propagate only within affected subtree.
PubSub (lib/bb/pub_sub.ex): Hierarchical message routing by path. Subscribers can match exact paths or entire subtrees.
Kinematics (lib/bb/robot/kinematics.ex): Forward kinematics using 4x4 homogeneous transform matrices (Nx tensors).
Runtime (lib/bb/robot/runtime.ex): Manages robot operational state with a state machine:
:disarmed→:idle→:executing→:idle- Commands only execute in allowed states
- Subscribes to sensor messages and updates joint positions
Commands: Short-lived GenServers defined in the DSL commands section. Handlers use use BB.Command and implement handle_command/3 and result/1 callbacks. Commands can react to messages during execution and handle safety state changes. Built-in commands include BB.Command.Arm and BB.Command.Disarm.
URDF Export (lib/bb/urdf/exporter.ex): Converts robot definitions to URDF XML format for use with ROS tools like RViz and Gazebo. Available via mix bb.to_urdf.
Robots can run in simulation mode without hardware:
# Start in kinematic simulation
MyRobot.start_link(simulation: :kinematic)
# Check simulation mode
BB.Robot.Runtime.simulation_mode(MyRobot) # => :kinematic or nilIn simulation mode:
- Actuators are replaced with
BB.Sim.Actuatorwhich publishesBeginMotionmessages with timing based on joint velocity limits - Controllers are omitted by default (configurable per-controller with
simulation: :omit | :mock | :start) - Safety system still requires arming before commands work
OpenLoopPositionEstimatorworks unchanged for position feedback
See documentation/tutorials/10-simulation.md for details.
See documentation/topics/understanding-safety.md for comprehensive safety documentation.
Changes to safety-critical code require extra care - bugs here could result in physical harm.
Key points:
- Components that drive hardware implement a
disarm/1callback defined on their behaviour:BB.Actuator(required),BB.Controller/BB.Sensor(optional).BB.Safetyis the arm/disarm API, not a behaviour. - Components register with the safety system via
BB.Safety.register/2 disarm/1is called with the opts given at registration and must work without GenServer state (process may have crashed)- Safety states:
:disarmed→:armed→:disarming→:disarmed(or:erroron failure) - Disarm callbacks run concurrently with 5 second timeout
- The
:errorstate means hardware may not be safe - requiresforce_disarm/1to recover
BB uses structured errors via BB.Error (built on Splode). All error types must implement the BB.Error.Severity protocol.
Error classes:
:hardware- Communication failures with physical devices:safety- Safety system violations (always:criticalseverity):kinematics- Motion planning failures:invalid- Configuration and validation errors:state- State machine violations (command not allowed, timeout, preempted):protocol- Low-level protocol failures (Robotis, I2C, etc.)
Severity levels:
:critical- Immediate safety response required:error- Operation failed, may retry or degrade:warning- Unusual condition, operation continues
Creating new error types:
defmodule BB.Error.State.MyError do
use BB.Error, class: :state, fields: [:field1, :field2]
defimpl BB.Error.Severity do
def severity(_), do: :error
end
def message(%{field1: f1, field2: f2}) do
"Descriptive message: #{inspect(f1)}, #{f2}"
end
endPrefer structured errors over tuples - Use {:error, %BB.Error.State.NotAllowed{}} instead of {:error, {:not_allowed, reason}}.
BB.Message wraps payloads with timestamp/frame_id. Payload types use use BB.Message with a schema for validation via Spark.Options.
Payload naming conventions under BB.Message.Sensor.* and BB.Message.Actuator.*:
*State— multi-field snapshot of an identifiable entity's current condition. Used when several coupled fields together describe the entity at one moment in time. Examples:BatteryState,JointState,PowerState.- Naked noun — a single-purpose reading where the message is the sample. Examples:
Image,Range,LaserScan. (Imulives here too although it could have beenImuState— kept as-is for precedent.) VerbObject— events / notifications. Example:BeginMotion. Future likely candidates:EndMotion,Stalled,LimitReached.
When adding a new payload, pick the convention that matches its shape rather than inventing a new suffix.
- Units: Use
Localize.Unitthroughout DSL, converted to floats (SI) in Robot struct - Transforms: 4x4 matrices in
BB.Math.Transform, angles in radians - Process registration: Uses Registry with
:viatuples, names must be globally unique per robot - DSL entities are structs in
lib/bb/dsl/matching entity names - Commands: Return
{:ok, result}or{:ok, result, next_state: state}for state transitions - State machine: Robots start
:disarmed, transition to:idlewhen armed,:executingduring commands - Errors: Use structured
BB.Errortypes instead of tuple-based errors. All errors must implementBB.Error.Severity - Safety: Components that drive hardware implement the
disarm/1callback from their behaviour (BB.Actuatorrequired;BB.Controller/BB.Sensoroptional) and register viaBB.Safety.register/2. Test disarm callbacks thoroughly - they run when things have already gone wrong
Feature proposals for new packages are tracked in the proposals repository. Check there for planned features and their design documents.
Every source file must carry an SPDX header — a #-style comment for code, an
HTML comment for Markdown, or a <file>.license sidecar for files that can't
hold comments (binaries, JSON, lockfiles). mix check runs reuse lint and
fails the build if one is missing.
When you create a new file, its SPDX-FileCopyrightText line must credit the
user you are working for — not you (the agent), and not this repo's original
author. Take their name from git config user.name (add their user.email if
you include one) and use the current year. Match the neighbouring files'
SPDX-License-Identifier (usually Apache-2.0):
SPDX-FileCopyrightText: <current year> <your user's name>
SPDX-License-Identifier: Apache-2.0
Never copy an existing file's copyright line onto a new file — that credits the wrong person. When you only edit an existing file, leave its headers unchanged.