Architecture & Project Model
This page explains how raccoon is structured as a framework and how a project maps onto the running robot. Read this before diving into individual features — the mental model pays for itself many times over.
The Layered Stack
graph TB
subgraph "You Write This"
M["Mission code (Python)"]
S["Step DSL — drive_forward, servo, on_black, ..."]
end
subgraph "raccoon handles this"
MC["Motion Controller\ntrapezoidal velocity profiles"]
DR["Drive + Velocity PID\nper-axis PID + feedforward"]
KI["Kinematics\nDifferential or Mecanum"]
OD["Odometry\nplatform-managed (lazy @property)"]
HAL["HAL — Motor / Servo / IR / IMU\nabstract interfaces"]
end
subgraph "Platform"
WOM["Wombat Driver\nLCM IPC to STM32 firmware"]
MOCK["Mock Driver\nfor testing without hardware"]
end
M --> S
S --> MC
MC --> DR
DR --> KI
KI --> HAL
DR --> HAL
OD --> HAL
HAL --> WOM
HAL --> MOCK
style M fill:#4CAF50,color:#fff
style S fill:#66BB6A,color:#fff
style MC fill:#42A5F5,color:#fff
style DR fill:#42A5F5,color:#fff
style KI fill:#42A5F5,color:#fff
style OD fill:#42A5F5,color:#fff
style HAL fill:#AB47BC,color:#fff
style WOM fill:#FF7043,color:#fff
style MOCK fill:#FF7043,color:#fff
You spend 90% of your time in the top two layers. The layers below run automatically at 100 Hz during every drive step; you interact with them only when tuning PIDs or writing advanced custom steps.
The Project Model
Every raccoon project has the same structure. The key mental model is:
YAML is the single source of truth. Python is generated from YAML. You own the missions.
graph LR
YAML["raccoon.project.yml\n+ config/*.yml"]
CLI["raccoon run / raccoon codegen"]
GEN_DEFS["defs.py — GENERATED\nall hardware as Python objects"]
GEN_ROBOT["robot.py — GENERATED\nkinematics + drive + mission list"]
MAIN["main.py\nRobot().start()"]
MISSIONS["src/missions/*.py\nyours to write and edit"]
STEPS["src/steps/*.py\nyours to write and edit"]
YAML -->|"read by"| CLI
CLI -->|"generates"| GEN_DEFS
CLI -->|"generates"| GEN_ROBOT
GEN_DEFS -->|"imported by"| GEN_ROBOT
GEN_ROBOT -->|"imported by"| MAIN
GEN_ROBOT -->|"references"| MISSIONS
MISSIONS -->|"may import"| STEPS
style YAML fill:#FFA726,color:#fff
style CLI fill:#42A5F5,color:#fff
style GEN_DEFS fill:#AB47BC,color:#fff
style GEN_ROBOT fill:#AB47BC,color:#fff
style MAIN fill:#4CAF50,color:#fff
style MISSIONS fill:#66BB6A,color:#fff
style STEPS fill:#66BB6A,color:#fff
Rules:
- Edit
raccoon.project.yml(andconfig/*.yml) to change hardware, kinematics, PIDs, or physical geometry. - Run
raccoon run— it regeneratesdefs.pyandrobot.pyautomatically, then executes. - Write missions and steps freely — the codegen never touches those files.
- Never edit
defs.pyorrobot.pyby hand — your changes will be overwritten.
From YAML to Running Robot
Here is what happens from the moment you type raccoon run to the moment the robot moves:
sequenceDiagram
participant You
participant CLI as raccoon run
participant Codegen as Code Generator
participant Sync as SFTP Sync
participant Pi as Raspberry Pi
participant Robot as robot.start()
You->>CLI: raccoon run
CLI->>Codegen: read raccoon.project.yml
Codegen->>Codegen: generate defs.py + robot.py
CLI->>Sync: sync changed files to Pi
CLI->>Pi: execute src/main.py
Pi->>Robot: Robot().start()
Robot->>Robot: Platform.probe() — verify STM32 + IMU
Robot->>Robot: run setup_mission (calibrate, home)
Robot->>Robot: pre_start_gate (wait for light/button)
loop Each main mission
Robot->>Robot: run mission.sequence()
end
Robot->>Robot: run shutdown_mission
Robot->>Robot: disable all motors
The probe step (Platform.probe()) verifies that the STM32 firmware and IMU are reachable before any motors move. It fails fast so you find hardware problems immediately, not mid-mission.
The Robot Definition: Defs + Robot
Two Python classes form the backbone of every project:
Defs — Hardware Registry
A plain Python class with every physical component as a class-level attribute. You never instantiate Defs — you reference Defs.front_left_motor directly:
class Defs:
imu = IMU()
button = DigitalSensor(port=10)
front_left_motor = Motor(port=0, inverted=False, calibration=MotorCalibration(...))
front_right_ir = IRSensor(port=0)
front = SensorGroup(right=front_right_ir)
arm = ServoPreset(Servo(port=1), positions={"up": 32, "down": 160})
analog_sensors = [front_right_ir] # REQUIRED for IR calibration
analog_sensorsis required. TheRobotDefinitionsProtocolmandates it. Without it,calibrate()silently skips IR sensor calibration.
Robot — Integration Layer
A subclass of GenericRobot that wires kinematics, drive, odometry, and missions together. The odometry attribute is a lazy @property that calls Platform.create_odometry(kinematics) on first access — never a manually constructed object:
class Robot(GenericRobot):
defs = Defs()
kinematics = DifferentialKinematics(...)
drive = Drive(kinematics=kinematics, ...)
# odometry: @property — platform-managed, not listed here
shutdown_in = 120
missions = [M010FirstMission()]
setup_mission = M000SetupMission()
shutdown_mission = M999ShutdownMission()
Defs Relationship Diagram
graph TD
DEFS["Defs class\nhardware inventory"]
MOTOR["Motor objects\nport + calibration"]
SERVO["ServoPreset objects\nnamed positions + offset"]
SENSOR["IRSensor / AnalogSensor\ncalibration thresholds"]
SGROUP["SensorGroup\nleft + right sensors"]
ANALOG_LIST["analog_sensors list\nrequired for calibration"]
ROBOT["Robot(GenericRobot)\nintegration layer"]
DRIVE["Drive\nvelocity PID"]
KIN["Kinematics\nDiff or Mecanum"]
MISSIONS["Mission list"]
DEFS --> MOTOR
DEFS --> SERVO
DEFS --> SENSOR
SENSOR --> SGROUP
DEFS --> ANALOG_LIST
ROBOT --> DEFS
ROBOT --> DRIVE
DRIVE --> KIN
KIN --> MOTOR
ROBOT --> MISSIONS
style DEFS fill:#AB47BC,color:#fff
style ROBOT fill:#AB47BC,color:#fff
style DRIVE fill:#42A5F5,color:#fff
style KIN fill:#42A5F5,color:#fff
style MISSIONS fill:#66BB6A,color:#fff
Mission Execution Lifecycle
stateDiagram-v2
[*] --> Setup: robot.start()
Setup --> PreStartGate: sequence() complete
PreStartGate --> Running: light signal / button
Running --> NextMission: mission complete
NextMission --> Running: more missions
NextMission --> Shutdown: no more missions
Running --> Shutdown: shutdown_in timer fires
Running --> Shutdown: mission.time_budget fires
Shutdown --> [*]: all motors disabled
Key rules:
- Setup mission must subclass
SetupMission(notMission). If you getTypeError: setup_mission must be a SetupMission, this is why. shutdown_inis a last-resort safety timer (required by Botball rules). Your missions should complete well before it fires.time_budgeton aMissionsubclass adds a per-mission watchdog. When it fires, the shutdown mission runs and subsequent missions are skipped.
Import Convention
Always use from raccoon import * in mission and step files. The older from libstp import * is a compatibility shim that will be removed:
# Correct — use this in all new code
from raccoon import *
from src.hardware.defs import Defs
# Deprecated — still works but emits DeprecationWarning
from libstp import * # ← do not use
Further Reading
| Topic | Page |
|---|---|
Full YAML reference for raccoon.project.yml | Configuration Reference |
Defs class patterns, ServoPreset, SensorGroup | Robot Definition |
| Writing your first mission | Your First Robot Program |
YAML includes and !include-merge | YAML Includes |
| Motion profiles and control loop internals | Motion Flow and Kinematics |