CrashPilot is the main Rust controller for our RoboCup Small Size League (SSL) team. It sits between SSL-Vision, the SSL GameController, the operator interface, and the robots. Every control tick it collects the latest world and operator state, updates an internal model of the match, chooses robot commands, and sends those commands to the configured robots over UDP.
The controller is built around the core_dump protobuf types used by the team.
Incoming SSL packets, interface commands, robot feedback, and outgoing robot
commands are all encoded as protobuf messages.
- Receives SSL-Vision raw geometry and detection packets over multicast.
- Receives SSL-Vision tracked packets over multicast.
- Receives SSL referee packets from the GameController multicast stream.
- Optionally connects to the SSL GameController team TCP protocol.
- Hosts a WebSocket endpoint for the local operator interface.
- Receives robot feedback packets over UDP and tracks robot heartbeats.
- Builds a normalized
WorldStatefrom vision, referee, interface, and robot feedback data. - Runs manual, game, or test control logic.
- Adapts AI decisions from the
artificial_incompetencecrate into low-level robot commands. - Sends
CpRobotcommand packets to every reachable configured robot. - Optionally exposes Prometheus metrics and pushes robot command logs to Loki.
.
|-- src/
| |-- main.rs # CLI entry point
| |-- lib.rs # CrashPilot runtime and control loop
| |-- config.rs # TOML config loading and defaults
| |-- communication.rs # Communication task wiring
| |-- communication/
| | |-- ssl_communication.rs # SSL-Vision and referee multicast input
| | |-- ssl_gc_handler.rs # SSL GameController team TCP protocol
| | |-- interface.rs # WebSocket interface server
| | |-- robot_receiver.rs # Robot feedback UDP input
| | |-- robot_sender.rs # Robot command UDP output
| | |-- create_multicast_socket.rs # Multicast socket setup
| | |-- udp_listener.rs # Generic protobuf UDP listener
| | `-- loki.rs # Optional Loki publisher
| |-- game_logic.rs # Mode dispatcher
| |-- game_logic/
| | |-- types.rs # WorldState, Robot, BallData, referee state
| | |-- mode_manual.rs # Direct operator command mode
| | |-- mode_game.rs # Match/game strategy mode
| | |-- mode_test.rs # Robot hardware test mode
| | |-- ai_handler.rs # AI command adapter
| | |-- ball_placement.rs
| | |-- defend.rs
| | `-- kick_off.rs
| |-- helpers/ # Robot data and geometry helpers
| |-- metrics.rs # Optional Prometheus endpoint
| |-- interface.rs # Optional embedded interface launcher
| `-- utils.rs # Shared runtime structs and socket setup
|-- crates/
| |-- artificial_incompetence/ # AI model, inference, and training code
| `-- strategy_presets/ # Strategy preset helpers
|-- grafana/ # Example Grafana dashboards
|-- config.toml # Local runtime configuration
|-- Cargo.toml
`-- README.md
CrashPilot is centered on the CrashPilot struct in src/lib.rs.
SSL-Vision raw multicast --\
SSL-Vision tracked ---------\
SSL GameController ref ------+--> Events --> recv --> update_data --> update_logic --> send
Interface WebSocket --------/
Robot UDP feedback --------/
| |
| |--> UDP CpRobot to robots
| `--> WebSocket snapshot to UI
`--> WorldState + AI state
Communication tasks write the newest packets into a shared Events value. The
main loop drains that latest snapshot on every tick, so slow or bursty producers
do not build an unbounded queue of old world states.
The main loop currently runs every 8 ms:
let mut tick = interval(Duration::from_millis(4));Each tick performs:
recv()drains latest incoming events fromEventShare.interpret()stores packets inPacketBuffer, updates selected team/side, updates robot feedback, and applies operator commands.update_data()converts tracked vision intoWorldStateand the AI-facing game state.update_logic()dispatches to manual, game, or test logic.send()sends robot UDP commands and publishes a WebSocket snapshot.
CrashPilot loads config.toml from the current working directory. If the file
does not exist, config::load_or_create_config("config.toml") writes a default
configuration.
The main config sections are:
[ssl]
ssl_vision_raw_ip = "224.5.23.2"
ssl_vision_raw_port = 10006
ssl_vision_tracked_ip = "224.5.23.2"
ssl_vision_tracked_port = 10010
ssl_gc_ip = "224.5.23.1"
ssl_gc_port = 10003
ssl_gc_msg_ip = "127.0.0.1"
ssl_gc_msg_port = 10008
[server]
robot_socket_host = "0.0.0.0"
robot_socket_port = 8192
robots_port = 1024
robot_receive_port = 2048
websocket_host = "0.0.0.0"
websocket_port = 4096
[logging]
prometheus_host = "0.0.0.0"
prometheus_port = 9090
loki_host = "10.0.64.2"
loki_port = 3100
[robots]
0 = { ip = "10.0.64.100", substitution_pos = { x = 400, y = 0 } }
1 = { ip = "10.0.64.101", substitution_pos = { x = 800, y = 0 } }
4 = { ip = "10.0.64.104", port = 1025, substitution_pos = { x = 2000, y = 0 } }Important ports:
| Setting | Purpose |
|---|---|
ssl_vision_raw_ip:ssl_vision_raw_port |
SSL-Vision raw detection and geometry multicast input |
ssl_vision_tracked_ip:ssl_vision_tracked_port |
SSL-Vision tracker multicast input |
ssl_gc_ip:ssl_gc_port |
SSL GameController referee multicast input |
ssl_gc_msg_ip:ssl_gc_msg_port |
Optional GameController team TCP endpoint |
robot_socket_host:robot_socket_port |
Local UDP socket used to send robot commands |
robots_port |
Default destination UDP port on each robot |
robot_receive_port |
Local UDP port for robot feedback |
websocket_host:websocket_port |
Operator interface WebSocket endpoint |
Robot IDs are configured as TOML table keys under [robots]. Each robot needs
an IP address and a substitution_pos, used during timeout/substitution
positioning. A robot may also set port to override server.robots_port for
that robot only.
Install a recent Rust toolchain with edition 2024 support, then build:
cargo buildBuild with optional integrations:
cargo build --features interface
cargo build --features ssl_game_controller
cargo build --features prometheus
cargo build --features loki
cargo build --features "interface ssl_game_controller prometheus loki"The interface feature embeds and starts the crashpilot-interface binary from
the repository root. That file must be a compiled executable-compatible binary
for the target system.
Run with default AI:
cargo runRun with an AI checkpoint:
cargo run -- --ai-checkpoint path/to/model-or-checkpoint--ai-checkpoint and --ai-model are aliases. The path may point to a
model.safetensors file, a checkpoint directory, or a run directory containing
checkpoint_* directories.
Embedded callers can also use:
CRASHPILOT_AI_CHECKPOINT=path/to/checkpoint cargo runRun with common competition integrations:
cargo run --features "interface ssl_game_controller prometheus"The vision-filter-replay workspace binary opens an SSL .log or .log.gz
file, runs its raw and tracked packets through CrashPilot's current world model,
and starts the existing web interface. It does not start the controller or any
robot sockets.
cargo run --release -p vision-filter-replay -- path/to/match.log.gzThe interface opens at http://127.0.0.1:8080. Use its Raw SSL, Tracked, and CrashPilot filter buttons to compare the recorded sources with the filtered result. Raw-camera and tracked-source selectors work exactly as they do for live vision.
Playback is controlled from the terminal:
| Key | Action |
|---|---|
Space or p |
Play or pause |
+ / - |
Double or halve playback speed |
Left / Right |
Seek backward or forward one second |
Shift+Left / Shift+Right |
Seek backward or forward ten seconds |
, / . |
Step to the previous or next log event |
Home / End |
Jump to the start or end |
r |
Restart and play |
? |
Show all controls |
q, Esc, or Ctrl+C |
Quit |
Useful startup options include --paused, --speed 4, --no-browser,
--interface-port, --websocket-port, and --config. Run with --help for
the complete CLI. Match payloads are spooled to a temporary on-disk cache so
compressed logs remain seekable without retaining every decoded protobuf in
memory; the cache and embedded interface process are removed on exit.
The replay executable is a separate workspace package under
tools/vision-filter-replay. Its Loguna, Clap, and terminal dependencies are
not dependencies of the crashpilot package, and crashpilot remains the
workspace's default build member.
| Feature | Effect |
|---|---|
interface |
Starts the embedded crashpilot-interface binary alongside the controller. |
ssl_game_controller |
Enables the SSL GameController team TCP protocol handler and goalie requests. |
prometheus |
Starts an HTTP metrics server with /metrics and /health. |
loki |
Publishes outbound robot command logs to Loki. |
src/communication/ssl_communication.rs creates multicast sockets for:
- raw SSL-Vision packets as
SslWrapperPacket - tracked SSL-Vision packets as
TrackerWrapperPacket - referee packets as
Referee
Each listener decodes protobuf UDP datagrams and stores only the latest packet
of each type in Events.
src/communication/interface.rs hosts a WebSocket server at
server.websocket_host:server.websocket_port.
Incoming binary WebSocket messages are decoded as InterfaceWrapperCp and used
for:
- robot commands in manual mode
- selected controller mode
- test selection and selected test robots
- team color and field side
- goalkeeper ID
- optional manual use of referee halt/stop commands
Outgoing messages are encoded as CpInterfaceWrapper and include:
- latest raw vision packet
- latest tracked vision packet
- latest referee packet, if available
- latest command packet for every robot
Outbound WebSocket publishing keeps only the newest snapshot. If a client is slow, it skips stale frames instead of delaying the controller.
src/communication/robot_receiver.rs binds
server.robot_socket_host:server.robot_receive_port and expects UDP protobuf
RobotCp feedback messages from known robot IPs.
When feedback is received from a configured robot IP:
- the robot heartbeat timestamp is updated
- the decoded feedback packet is stored in
Events.rf - fields such as
has_ball, battery voltage, kicker readiness, and acting status become available to game logic and metrics
src/communication/robot_sender.rs sends one CpRobot protobuf packet to each
configured robot at:
robot.ip:(robot.port or server.robots_port)
Sending is best-effort per robot. A failure for one robot does not stop sends to other robots.
A robot is considered reachable only if its latest heartbeat is less than 100 ms old. If the heartbeat is older, the sender reports the robot as unreachable and does not send a command packet to it.
src/game_logic/types.rs defines the internal match model:
WorldStatestores own robots, opponent robots, ball data, referee data, interface commands, active goalie, defenders, and current phase.Robotstores robot position, velocity, orientation, angular velocity, distance to the ball, distance to teammates/opponents, and wall distances.BallDatastores current ball position/velocity and kicked-ball stop prediction when the tracker provides it.RefMachinemaps SSL referee commands into a smaller internal state machine.GamePhasedescribes controller phases such as halted, stopped, kickoff, penalty, free kick, running, timeout, and ball placement.
Coordinates and velocities are handled in SSL field units:
- positions: millimeters
- linear velocities: millimeters per second
- orientations: degrees
- angular velocities: degrees per second
CrashPilot keeps a bounded, per-source history for every raw SSL camera and every tracked-vision publisher (identified by source name and UUID). The history is evaluated on the vision timestamps, so replay and accelerated simulation do not depend on wall-clock speed. Raw and tracked observations at the same logical time are grouped before fitting to avoid treating an autoref's already-filtered output as independent extra evidence.
The filter produces one authoritative tracked frame used by the AI and sent to the robots. It estimates ball and robot position/velocity with robust linear fits, switches to a quadratic ball fit for sustained curvature, uses outbound commands as a bounded prior for own robots, and detects/learns wall bounces. The filtered frame and component uncertainty are also published to the interface alongside the original raw and tracked sources.
[world_model] in config.toml controls history/fit windows, prediction
timeouts, physical limits, and wall priors. Set enabled = false to restore
the legacy direct tracked-vision path.
Field dimensions come from SSL-Vision geometry when available. Before geometry
arrives, FieldSetup::default() assumes a 9000 mm by 6000 mm field.
The active mode comes from InterfaceCommandCp.mode.
Implemented in src/game_logic/mode_manual.rs.
Manual mode copies per-robot commands received from the WebSocket interface directly into the outgoing robot command packets. If manual GameController control is enabled by the interface and the referee command is halt or stop, the robot command state is overridden with that referee state.
Use this mode for operator-driven movement and direct robot debugging.
Implemented in src/game_logic/mode_game.rs.
Game mode is the main autonomous match mode. It uses WorldState.phase to
decide how to command the team:
- halted and stopped phases set all robots to halt/stop
- kickoff, penalty, and free-kick phases keep the goalie in goalie state
- timeout sends robots to configured substitution positions
- ball placement assigns the closest own robot to place the ball
- running phase sets the goalie, handles goalie possession, and otherwise calls the AI handler
When the goalie has the ball, CrashPilot tries to chip to the farthest teammate. If no good teammate target exists, it uses the goal-shooting helper.
With the ssl_game_controller feature enabled, a changed goalkeeper selection
causes CrashPilot to send a desired_keeper request to the GameController team
protocol.
Implemented in src/game_logic/mode_test.rs.
Test mode provides hardware and behavior tests selected from the interface:
TestNone: halt selected robotsTestBallControl: run dribbler/ball-control behaviorTestDribbler: move selected robots to changing target positions with ball positioningTestKicker: with one robot, approach the ball; with two robots, alternate kick and receive behavior based on ball possession
If no robot IDs are selected by the interface, test mode applies to all configured robots.
The AI implementation lives in crates/artificial_incompetence.
CrashPilot stores AI-facing state in
artificial_incompetence::types::GameState. update_ai_data() normalizes robot
and ball data by field dimensions before inference.
src/game_logic/ai_handler.rs calls:
let commands = cp.ai.predict(&cp.ai_data, 1f32);It then converts AI RobotCommand values into low-level CpCommand tasks:
| AI command | Robot task/action |
|---|---|
Pos |
TaskPos |
Kick |
TaskKick |
Chip |
kick/chip command path |
RecKick / RecPass |
TaskRecKick |
Steal |
TaskSteal |
Dribble |
TaskDribble |
PosBall |
TaskPosBall |
KickGoal |
best-angle shot helper |
PassTo |
kick toward selected teammate |
GoalWall |
add robot to defenders |
GoalieGuard |
TaskBlock |
Hold |
zero speed |
The goalie is excluded from normal AI command application when a goalie ID is known.
Enable with:
cargo run --features prometheusThe metrics server listens on:
http://<logging.prometheus_host>:<logging.prometheus_port>
Endpoints:
/metrics: Prometheus text exposition/health: simple health check
Metrics include registered robots, feedback presence, battery voltage, current, kicker readiness, ball possession, error/acting flags, packet counters, send success/failure counters, and tracked robot velocities.
Example Grafana dashboards are stored in grafana/.
Enable with:
cargo run --features lokiOutbound robot command packets are batched and pushed to:
http://<logging.loki_host>:<logging.loki_port>/loki/api/v1/push
Logs are labeled with:
app="CrashPilot"direction="outbound"robot_id="<id>"
Enable with:
cargo run --features ssl_game_controllerCrashPilot connects to ssl.ssl_gc_msg_ip:ssl.ssl_gc_msg_port, registers as:
Robocup Junior SSL Team
The handler supports:
- desired keeper requests
- advantage choice
- substitution requests
- ping
Replies from the GameController team protocol are stored in
Events.gc_team_messages.
Format:
cargo fmtCheck:
cargo checkTest:
cargo testBuild optimized:
cargo build --releaseCross-build examples depend on the local cross toolchain and target setup. This repository has previously been used with:
cross build --target aarch64-unknown-linux-gnu --release- The controller loop comment says
~500 Hz, but the configured 8 ms interval is approximately 125 Hz. WorldState::update_states()maps referee commands intoGamePhase, then currently forcesself.phase = GamePhase::Runningat the end. That means the live game-mode behavior always executes the running branch until that testing override is removed.- Robot UDP sending depends on recent feedback heartbeats. If robots are not
sending
RobotCpfeedback torobot_receive_port, CrashPilot will mark them unreachable and skip command sends. - The outbound WebSocket and event handling intentionally keep the latest state rather than a full history. This is good for real-time control, but it means packet-by-packet replay needs a separate logger.
config.tomlmay contain operational comments or extra keys. The Rust config structs deserialize only the fields defined insrc/config.rs.
- Confirm network interface and multicast routing are correct for SSL-Vision and the GameController.
- Confirm
config.tomlrobot IPs match the robots on the field. - Confirm each robot sends feedback to
server.robot_receive_port. - Start SSL-Vision and the SSL GameController.
- Start CrashPilot with the required features.
- Open/connect the operator interface to the WebSocket endpoint.
- Select team color, field side, goalie, and controller mode in the interface.
- Verify robot heartbeats, vision tracking, and referee state before enabling autonomous game behavior.