YAML Includes
Concept: Why Config Is Split Across Files
RaccoonOS projects split configuration across several files not just for tidiness, but because different tools own different files:
raccoon.project.yml— the root; references all sections via!includeconfig/robot.yml— motion PID, kinematics, shutdown timer; edited by the IDE PID tunerconfig/hardware.yml— sensor, motor, servo definitions; the codegen reads this to generatedefs.pyconfig/motors.yml/config/servos.yml— merged intohardware.ymlvia!include-merge; calibration steps write directly to these filesconfig/missions.yml— mission order; the mission list editor writes here
The loader resolves the full tree transparently so your Python code can read ["definitions", "left_motor", "calibration", "ticks_to_rad"] without caring which physical file stores it. The write-back system preserves ownership — a calibration update writes to motors.yml, not raccoon.project.yml, even though the read path went through the root.
graph TD
ROOT["raccoon.project.yml"] -->|"!include"| ROBOT["config/robot.yml"]
ROOT -->|"!include"| HW["config/hardware.yml"]
ROOT -->|"!include"| MISSIONS["config/missions.yml"]
HW -->|"!include-merge '_motors'"| MOTORS["config/motors.yml"]
HW -->|"!include-merge '_servos'"| SERVOS["config/servos.yml"]
MOTORS -.->|"calibration writes back here"| MOTORS
SERVOS -.->|"servo tuning writes back here"| SERVOS
Raccoon project configuration is not just split across files for convenience. The loader is explicitly include-aware, recursive, and write-aware.
This matters because tools like raccoon wizard, calibration steps, IDE routes, and programmatic config updates all rely on the same ownership rules.
Source of truth:
Two tags exist
!include
!include delegates a value to another YAML file.
Example:
robot: !include 'config/robot.yml'
missions: !include 'config/missions.yml'
definitions: !include 'config/hardware.yml'
connection: !include 'config/connection.yml'
Semantics:
- the included file replaces the tagged value
- resolution is recursive
- the included file may itself contain more
!includeor!include-mergetags
!include-merge
!include-merge merges the top-level keys of another mapping into the parent mapping.
Example:
button:
type: DigitalSensor
port: 10
_motors: !include-merge 'motors.yml'
_servos: !include-merge 'servos.yml'
Semantics:
- the merge key itself is not kept as a runtime config node
- the included file must resolve to a mapping
- its top-level keys are promoted into the parent mapping
That is why motors and servos appear at the same level as sensor definitions in the resolved definitions: map.
The Underscore-Prefix Convention
All competition projects use underscore-prefixed merge keys (_motors, _servos). This is a deliberate convention, not a requirement:
- the leading
_signals “this key is a merge anchor, not a real hardware definition” - it prevents confusion if someone scans the
definitions:namespace — prefixed names stand out as structural keys - the generated
defs.pynever references these merge keys; they dissolve at resolution time
If you omit the underscore (motors: !include-merge 'motors.yml'), the behavior is identical — but the key motors would be invisible in the resolved namespace (since !include-merge discards it), which can be confusing when reading the file later.
Resolution depth
The include resolver is fully recursive and supports includes at any nesting depth.
That is not a documentation convention. It is implementation behavior in the library loader.
The resolver also has a recursion cap of 20 levels to prevent infinite loops. Exceeding this depth raises an error during resolution.
Read behavior
Read APIs such as yaml_read(...) and read_project_value(...) resolve through the include graph automatically.
That means callers can ask for a logical property path like:
["robot", "motion_pid", "angular", "max_velocity"]
without caring which physical file owns that path.
Write behavior
Writes are ownership-aware.
The key guarantee is:
- tools update the file that owns the target value
- unrelated include structure is preserved
!includeand!include-mergetags are written back as tags, not flattened away
This is why a calibration step can update config/motors.yml even when the caller asked for a path through raccoon.project.yml.
Single-value write
from raccoon.project_yaml import update_project_value
# Writes ticks_to_rad into the file that owns
# definitions.left_motor.calibration.ticks_to_rad
update_project_value(
project_root,
["definitions", "left_motor", "calibration", "ticks_to_rad"],
0.004363,
)
Returns True on success.
Batch write (yaml_write_many / update_project_values)
When multiple config values need to be updated atomically — for example after a full calibration run — use the batch API. It groups all writes by the physical file that owns each path and performs one file round-trip per file, rather than one per value.
from raccoon.project_yaml import update_project_values
# Atomically update multiple motor calibration keys.
# Values destined for the same physical file are written in a single pass.
update_project_values(project_root, {
("definitions", "left_motor", "calibration", "ticks_to_rad"): 0.004363,
("definitions", "left_motor", "calibration", "bemf_offset"): -0.0041,
("definitions", "right_motor", "calibration", "ticks_to_rad"): 0.004312,
("definitions", "right_motor", "calibration", "bemf_offset"): -0.0038,
})
The updates argument is a dict[tuple[str, ...], Any] — key paths as tuples. Returns True if all writes succeeded.
update_project_values is a thin wrapper around yaml_write_many(project_root / "raccoon.project.yml", updates). If you already have the path to a different root file, call yaml_write_many directly:
from raccoon.project_yaml import yaml_write_many
from pathlib import Path
yaml_write_many(Path("config/motors.yml"), {
("left_motor", "calibration", "static_friction_pct"): 3.2,
("right_motor", "calibration", "static_friction_pct"): 2.8,
})
Why ordering matters
For humans, ordering is style.
For tooling, ownership is what matters:
raccoon.project.ymlowns top-level wiringconfig/hardware.ymlowns the parent mapping fordefinitionsmotors.ymlandservos.ymlown the keys merged intodefinitions
If you move keys across files arbitrarily, the loader can still resolve them, but you lose the predictable “one concern per file” structure that the wizard, codegen, and docs assume.
Practical rules
- Keep top-level sections in
raccoon.project.ymlas!includes. - Keep hardware subgroups that naturally flatten into one namespace as
!include-merges. - Do not hand-flatten everything into one file unless you are prepared to own the maintenance cost.
- If a tool writes a value to an unexpected file, check which file actually owns that path in the include graph.
What this page is for
This page is intentionally more technical than the project-structure docs. It exists so advanced users can reason about:
- why write-back landed in a particular file
- why merged keys appear “as if they were local”
- how the IDE and calibration steps update split config safely