Introduction
Dies is the Delft Mercurians' framework for RoboCup Small Size League. It runs on a central computer, receives camera and referee data from the league software, decides what our robots should do, and sends commands to them over radio. It also contains a physics simulator with an auto-referee, so the whole stack can run without any hardware.
The core is written in Rust. Strategies, the team-level logic, are separate Rust binaries that run in their own process and talk to the core over IPC, so they can be rebuilt and hot-swapped while Dies keeps running. A web UI shows the field, debug output and settings, and lets you control the executor and simulator.
- Setting Up: install the toolchain and run Dies.
- Codebase Map: follow one frame through the system and find where a change belongs.
- Developer Guide: conventions for git, code style, errors and logging.
The source lives at github.com/DelftMercurians/Dies.
Setting Up
Requirements
- A recent stable Rust toolchain, managed with rustup. Install
rustfmtandclippywithrustup component add rustfmt clippy. - On Linux:
sudo apt install libudev-dev libssl-dev pkg-config clang. pnpmandtypeshare(cargo install typeshare-cli) on yourPATH. Thedies-webuibuild script uses them to regenerate the TypeScript bindings and build the frontend bundle.
Running
Dies is a plain Cargo workspace. The dies-cli crate is the entry point; it also builds the strategy binary for you.
cargo run
Open the web UI at http://localhost:5555.
For a headless, faster-than-realtime match between two strategies (no UI, deterministic for a given seed):
cargo run -- self-play --blue-strategy concerto --yellow-strategy concerto
Web UI development
Run pnpm install in webui/ once, then start mprocs from the repository root. It runs the Vite dev server (with hot module reloading) next to dies --watch.
Tests and lints
cargo test # all tests
cargo test -p dies-world # one crate
cargo fmt && cargo clippy
cd webui && pnpm run tsc # TypeScript type check
Debugging hangs
Install tokio-console with cargo install --locked tokio-console and run it while Dies is running. It shows the state of every async task.
Codebase Map
The quickest way to find where a change belongs is to follow one frame of data through the system: from a camera packet to a robot command. The map below is organised along that path, not along the crate dependency graph.
flowchart TD
VIS[SSL Vision / Game Controller<br/>dies-ssl-client]
SIM[Simulator + auto-ref<br/>dies-simulator]
BS[Robot feedback<br/>dies-basestation-client]
TR[World tracking<br/>dies-world]
EX[Executor frame tick<br/>dies-executor]
STRAT[Strategy process<br/>StrategyHost - IPC - Strategy::update]
subgraph ctrl[TeamController, per robot]
SK[Skills] --> COMP[Rule compliance] --> PLAN[Path planning + ORCA] --> PC[PlayerController]
end
OUT[Robot commands<br/>basestation / simulator]
UI[Web UI<br/>dies-webui + webui/]
LOG[Binary log<br/>dies-logger]
VIS --> TR
SIM --> TR
BS --> TR
TR -- WorldData --> EX
EX -- WorldSnapshot --> STRAT
STRAT -- SkillCommands --> SK
EX -- WorldUpdate --> UI
UI -. ControlMsg .-> EX
EX --> LOG
PC -- PlayerCmd --> OUT
One frame, step by step
Everything is driven by a vision frame. In sim the loop steps the simulator and feeds its output through exactly the same entry points as real packets.
| # | Stage | Where | What happens |
|---|---|---|---|
| 1 | Receive | dies-ssl-client, dies-basestation-client, dies-simulator | Vision/referee packets arrive over UDP (or TCP); robot feedback (breakbeam, voltages, …) over the serial basestation. The simulator produces the same messages (detection, gc_message, feedback). |
| 2 | Dispatch | Executor (run_rt_live, run_rt_sim, step_simulation, run_headless) | The main loop in dies-executor selects on all inputs and calls update_from_vision_msg / update_from_gc_msg / update_from_bs_msg. |
| 3 | Track | WorldTracker | Kalman-filtered players and ball (filters), game state from referee commands, and possession. WorldTracker::get() yields a WorldData in absolute coordinates. |
| 4 | Frame tick | Executor::update_team_controller | The heart of the system. Runs the steps below in order, once per vision frame. |
| 4a | Publish | same | Broadcasts a WorldUpdate to the web UI (absolute coordinates). |
| 4b | To team frame | WorldData::get_team_data, sides.rs | Converts to a team-relative frame (+x always toward the opponent goal). Everything downstream of this point is team-relative. |
| 4c | Strategy | StrategyHost → transform.rs → IPC → runner → Strategy::update | Builds a WorldSnapshot, sends it to each team's strategy process, and collects SkillCommands, roles and debug output. Strategies use TeamContext / PlayerHandle to issue commands. |
| 4d | Skills | TeamController → SkillExecutor, JointSkillExecutor | Each SkillCommand becomes a running executable skill (GoToPos, HandleBall, Receive, …) that emits a PlayerControlInput per tick. Two-robot passes go through the pass coordinator. |
| 4e | Rules | comply in team_controller.rs | Enforces game-state rules on every robot's input (stop distances, defense-area keep-outs, ball placement). Yellow-carded robots are benched earlier, by plan_sidelining, before the strategy runs. |
| 4f | Motion | avoidance planner, ORCA, PlayerController, path follower, yaw control | Plans a path around obstacles, follows it, deflects through ORCA, applies acceleration/velocity limits and dribbler/kicker logic. Produces a PlayerCmd and transforms it back to absolute coordinates. |
| 4g | Log | dies-logger worker | Records the frame (world, debug values, raw feedback) to the columnar log. |
| 5 | Send | Executor::player_commands → BasestationHandle::send_no_wait / Simulation::push_cmd | Commands go out over radio, or into the simulator for its next step. |
Side channels
- Web UI control. The React app reads world/debug state over a WebSocket and sends commands via REST (
api.tsx). Backend:server.rs/routes.rs→UiCommand→executor_task.rs→ExecutorHandle/ControlMsg→Executor::handle_control_msg. Rust types reach TypeScript throughtypeshare(generated intowebui/src/bindings.tsbybuild.rs). - Settings and tunables.
ExecutorSettings(persisted indies-settings.json) configures tracker, controller and avoidance. Skill knobs use thetunables!macro (tunable.rs); strategy-declared parameters useparams.rs. All are editable live from the UI. - Debug output. Executor code draws/records with the global functions in
debug_info.rs; strategies usedies-strategy-api/src/debug.rs, which travels over IPC. Both end up in the UI and the log. - Logging and replay. Schema in
schema.rs; logs land inlogs/.replay.rs+replay_controller.rsplay a log back through the live UI channels. Offline analysis:tools/dieslog.pyandtools/match_analytics.py. - Strategy lifecycle. The CLI builds/watches strategy crates (
dies-cli/src/strategy.rs);StrategyHostspawns one process per team and hot-swaps it when the binary's mtime changes. - Headless self-play.
commands/self_play.rs→Executor::run_headless(headless.rs): a deterministic, faster-than-realtime sim match with no UI. - Entry point.
dies-cliparses flags (cli.rs) and starts the UI server, a self-play match, or a hardware test.
Where do I change…?
| I want to… | Start here |
|---|---|
| Change team behaviour: who goes where, when to shoot or pass | strategies/concerto: planner.rs (where the ball should go), driver.rs (turns a plan into skills), formation.rs (everyone else), keeper.rs, penalty.rs, constants in config.rs |
| Change how a robot does one thing (pick up the ball, aim, kick, receive) | dies-executor/src/skills/executable/, e.g. handle_ball/ |
| Add a new skill | Variant in SkillCommand → builder in PlayerHandle / skill_builders.rs → implementation in skills/executable/ → wire it up in create_skill_from_command (skill_executor.rs) |
| Change passing between two robots | pass_coordinator.rs, joint_skill_executor.rs, passing.rs |
| Expose new world information to strategies | WorldData → create_world_snapshot → WorldSnapshot → World accessor |
| Fix noisy or laggy robot/ball estimates | dies-world/src/player.rs, ball.rs, filter/, tuning in TrackerSettings |
| Change who is considered to have the ball | dies-world/src/possession.rs (types in dies-core/src/possession.rs) |
| Change how referee commands map to game states | dies-world/src/game_state.rs, GameState |
| Change rule enforcement (stop distance, defense area, yellow cards) | comply and plan_sidelining in team_controller.rs |
| Change obstacle avoidance or path planning | control/avoidance/ (planner.rs, orca.rs, obstacle.rs), config in avoidance_config.rs |
| Change trajectory tracking, acceleration limits, dribbler ramp | player_controller.rs, path_follower.rs, yaw_control.rs, motion_bounds.rs |
| Change what is sent to the robots / the radio protocol | PlayerCmd, basestation_client.rs |
| Change simulator physics or auto-referee | dies-simulator/src/lib.rs |
| Test a skill in isolation | strategies/scenarios (one binary per scenario) |
| Add a web UI panel or view | webui/src/components/panels/, webui/src/views/, data hooks in api.tsx |
| Add a UI → executor command | UiCommand → executor_task.rs → ControlMsg → Executor::handle_control_msg (lib.rs) |
| Add a REST endpoint | server.rs + routes.rs |
| Add a runtime-tunable knob | tunables! in the skill; generic settings in executor_settings.rs |
| Change what gets logged | schema.rs, builders.rs, worker.rs; reader in tools/dieslog.py |
| Add a CLI flag or subcommand | cli.rs, commands/ |
| Change in-match announcements | announcer.rs |
Ground rules
- Coordinates. Strategies and controllers work in team-relative coordinates (+x toward the opponent goal). The tracker, simulator, UI and log use absolute vision coordinates. Conversion happens only at the boundaries listed above. Never expose absolute coordinates to a strategy.
- Time. Anything in the per-frame path (skills, controllers, FSMs) uses world time (
ctx.world.dt,t_received), neverInstant. The headless sim runs faster than realtime, so a wall-clock timer fires at the wrong moment and breaks determinism. - Dependencies.
dies-coreholds shared types and nothing else. Feature crates depend on it, anddies-cliglues them together. Strategy crates only seedies-strategy-api.
Developer Guide
This document provides some guidelines for developing Dies.
Using Git
The main branch should remain stable at all times. All new features and fixes should be developed in feature branches, and merged into main only after they are complete and have been tested. Feature branches should be named <name of author>/<feature name>, eg. balint/fix-ersim-env-win.
The CI will run tests and formatting checks on all branches, but you should run these locally before pushing your changes.
Commit names should adhere to a lite version of the Conventional Commits spec, ie. they should follow the format: <commit type>: <short description>. Scopes are not needed. The commit type should be one of the following:
feat: new featurefix: bug fixrefactor: refactoringdocs: documentationtest: adding testschore: other changes (eg. CI, formatting, etc.)wip: work in progressmisc: other changes
Code Style
Dies uses the rustfmt formatter. It is recommended to use the rust-analyzer extension for VS Code, and configure it to automatically format the code on save.
We also use clippy for linting. The VS Code extension should also automatically run clippy on save.
In general, the Rust code should be idiomatic, and should attempt to follow the Rust API Guidelines.
Generics should be used with consideration. They are powerful, but they make the code more complex and significantly increase compile times. As a rule of thumb, remember YAGNI (you ain't gonna need it) -- don't overengineer, you can always add generics later if you need them.
Crate Structure
Dies is split into multiple crates. We use cargo workspaces to manage these. Having a lot of crates is not necessarily a bad thing -- crates are the unit of compilation in Rust, so more granular crates help caching and parallel compilation.
In general, the dependency graph between our crates should resemble a diamond:
+- feature crates -+
/ \
dies-core ------------> dies-cli
\ /
+- feature crates -+
There should be a few root crates (dies-core) which will rarely change, a number of specialized feature crates (dies-basestation-client, dies-executor, etc.), and a few small leaf crates which just glue things together (dies-cli).
For what each crate does, organised by data flow, see the Codebase Map.
Error Handling
Errors should be handled according to Rust idioms. In general, we should avoid panicking, and instead propagate Result to the very top, where it can be handled by the executor.
Throughout the codebase, the anyhow crate should be used for error handling. Anwhow provides a convenient dynamic wrapper type for errors, so we do not have to define our own error types. anyhow::Result should be the return type of all functions which can fail.
Logging
Logging is really important for Dies, as it is the only way to debug issues that occured during a match. Logs should contain all the information needed to replay matches, including all the data we receive from the vision system, the world state, and the commands we send to the robots.
Dies uses the log crate for logging. The following log levels should be used:
error: should be used only for fatal errors, which cause the executor to stop, ie. errors should only be logged in the executorwarn: should be used for unexpected events or recoverable errors, which do not cause the executor to stop and do not get propagated upinfo: should be used rarely, only for information that the user needs to know while running Dies - for example when long operations start and finish, or when error-prone operations succeeddebug: should be used most of the time: everything from the data we receive from the vision system, commands we send to the robots, to all minor events should be logged at this level
We do not use trace.
println! and all other forms of direct output should be avoided, as they are not captured by the logger.
Testing
Reliability is very important for Dies, so we aim for high test coverage. All new features should come with corresponding unit tests. Unit tests should test the code located in the module they are in, and should avoid relying on other modules.
Property-based testing should be explored for testing the core logic of Dies. See the proptest crate for more details on this technique.
Documentation
This section is WIP.