CODE ARCHITECTURE · SUBSYSTEM FILES · INTERNAL

Subsystem-File Architecture

πŸ—ΊοΈ Flowchart β€” How the .cpp files connect

main.cpp declares the hardware. subsystems.hpp re-declares it as extern so other files can reference it. autons.cpp and any new mechanism file consumes those externs.

flowchart LR
    main[main.cpp
declarations
initialize
opcontrol] sub[subsystems.hpp
extern declarations
extern pros::Motor arm
extern pros::Imu drive_imu] autons[autons.cpp
autonomous routines] arm[arm.cpp
arm-specific helpers
arm_init, arm_control] other[other .cpp files
any future mechanism] main -.->|"exposes via"| sub sub --> autons sub --> arm sub --> other style main fill:#0c4a6e,color:#e2e8f0,stroke:#22d3ee,stroke-width:2px style sub fill:#3f1d77,color:#e2e8f0,stroke:#a78bfa,stroke-width:2px style autons fill:#1e293b,color:#e2e8f0,stroke:#334155 style arm fill:#1e293b,color:#e2e8f0,stroke:#334155 style other fill:#1e293b,color:#94a3b8,stroke:#334155

After your code from the Bringup Center works, refactor it to per-mechanism files. main.cpp becomes a 10-line orchestration file. Subsystems are encapsulated, testable, and reusable across robots.

SECTION 1Why subsystem files

The Bringup Center teaches you to put everything in main.cpp — that's the right approach for learning one mechanism at a time. But once you have 3+ subsystems, main.cpp becomes a 400-line monster:

The fix is the same pattern professional embedded software uses: separation of concerns by subsystem. One .cpp file per mechanism. main.cpp becomes a thin orchestration layer that says "call the lift, call the intake, call the tube" in a loop — nothing more.

The 10-line opcontrol test: when your refactor is done, the opcontrol() function in main.cpp should fit in 10 lines or less, regardless of how many mechanisms the robot has. If it doesn't, mechanism control logic is still leaking into main — finish the refactor.

SECTION 2The file structure

Every Spartan robot project follows this layout. Files marked NEW are the ones we add beyond what the EZ-Template starter gives you.

src/ β”œβ”€β”€ main.cpp // initialize(), opcontrol(), autonomous() β€” orchestration ONLY β”œβ”€β”€ autons.cpp // PID constants + auton routines (separate, as before) β”œβ”€β”€ robot-config.cpp // motor, sensor, pneumatic definitions β€” single source of truth β”œβ”€β”€ arm/  or  lift/ // per-mechanism folder β€” each contains BOTH .hpp + .cpp β”‚ β”œβ”€β”€ arm.hpp // function declarations β”‚ └── arm.cpp // per-mechanism control logic β”œβ”€β”€ claw/ β”‚ β”œβ”€β”€ claw.hpp β”‚ └── claw.cpp β”œβ”€β”€ intake/ // (if equipped) β”‚ β”œβ”€β”€ intake.hpp β”‚ └── intake.cpp β”œβ”€β”€ tube/ // (advanced robots β€” non-blocking state machine) β”‚ β”œβ”€β”€ tube.hpp β”‚ └── tube.cpp └── pneumatics/ // (if equipped) β”œβ”€β”€ pneumatics.hpp └── pneumatics.cpp include/ // only project-wide headers β€” subsystem .hpps moved to src/<name>/ β”œβ”€β”€ main.h // master include β€” pulls in EVERY subsystem header via path β”œβ”€β”€ autons.hpp // auton function declarations (project-wide) └── robot-config.hpp // extern declarations for hardware (project-wide)
Why subsystem folders: co-locating each mechanism's .hpp + .cpp in its own folder under src/ makes ownership crystal clear. Everything about the arm lives in src/arm/. To find or modify a subsystem, you open one folder — no jumping between src/ and include/. Only project-wide headers (the master main.h, the shared robot-config.hpp, the auton declarations in autons.hpp) stay in include/.
Build system requirement: PROS recursively compiles all .cpp under src/ automatically (uses recursive wildcard), so adding subsystem folders just works for compilation. For headers in src/<name>/ to be discoverable from main.cpp and other files, add one line to the project Makefile: EXTRA_INCDIR := $(ROOT)/src. The reference projects already have this configured.

SECTION 3The four convention rules

These rules pay off as the codebase grows. Stick to them and code from Clawbot becomes copy-pasteable to Flex, code from Flex becomes copy-pasteable to a competition robot.

Every subsystem exposes <name>_init() and <name>_control(master)Predictable. Anyone reading main.cpp knows what to expect: each mechanism has exactly one init call and one control call.
State variables are static at file scopeCompiler-enforces "this state belongs to this subsystem." If main.cpp tries to write to claw_open, you get a linker error — proving the encapsulation.
Power and timing constants live at the top of the subsystem fileTuning happens by opening one file, scrolling to the top. LIFT_POWER, TUBE_PULSE_TICKS, etc. are visible immediately.
robot-config.cpp is the ONLY place motor / sensor objects are definedSingle source of truth for port assignments. All other files use extern declarations via robot-config.hpp.

SECTION 4main.cpp + robot-config — works for every robot

This skeleton works for Clawbot, Flex, and any competition robot. The only thing that changes per-robot is which subsystem files exist and which control functions are called in the opcontrol loop.

The thin main.cpp

Notice what's NOT here: no mechanism motor commands, no button-press logic, no state variables for toggles. All of that lives inside the subsystem files.

FILE src/main.cpp ORCHESTRATION ONLY β€” no mechanism logic
#include "main.h"

void initialize() {
  pros::lcd::initialize();
  pros::lcd::print(0, "Calibrating IMU...");

  chassis.drive_imu_calibrate();
  chassis.drive_brake_set(pros::E_MOTOR_BRAKE_COAST);

  // Each subsystem initializes its own brake mode, current limits, encoders, etc.
  // Comment out a line here to test that mechanism is disabled.
  arm_init();         // or lift_init() depending on the robot
  claw_init();
  // intake_init();   // uncomment per robot
  // tube_init();     // uncomment per robot
  // pneumatics_init();

  default_constants();
  ez::as::initialize();
  ez::as::auton_selector.autons_add({
    Auton("Drive + Turn (Ex 1)",  exercise1_drive_and_turn),
    Auton("Drive a Square (Ex 2)", exercise2_square),
  });
}

void disabled()               {}
void competition_initialize() {}

void autonomous() {
  chassis.pid_targets_reset();
  chassis.drive_imu_reset();
  chassis.drive_sensor_reset();
  chassis.drive_brake_set(pros::E_MOTOR_BRAKE_HOLD);
  ez::as::auton_selector.selected_auton_call();
}

void opcontrol() {
  pros::Controller master(pros::E_CONTROLLER_MASTER);

  while (true) {
    // Drive (one line β€” split arcade or tank, your call)
    chassis.opcontrol_tank();
    // chassis.opcontrol_arcade_standard(ez::SPLIT);   // for a competition robot

    // Mechanisms (one line each β€” add or remove per robot)
    arm_control(master);
    claw_control(master);
    // intake_control(master);
    // tube_control(master);
    // pneumatics_control(master);

    pros::delay(20);
  }
}
The 10-line opcontrol test: the while-loop body here has 4 lines for a 2-mechanism robot, 6 lines for a 5-subsystem robot. That's the goal — the whole control loop fits on one screen.

The master include — main.h

One line per subsystem header. When you add a new mechanism, you add one #include here and the rest of the codebase can see it.

FILE include/main.h Master include β€” add one #include per subsystem
#pragma once

// PROS + EZ-Template framework
#include "api.h"
#include "EZ-Template/api.hpp"

// Hardware definitions (extern declarations)
#include "robot-config.hpp"

// Auton declarations
#include "autons.hpp"

// Subsystem control headers β€” path-qualified since headers live in src/<name>/
#include "arm/arm.hpp"           // or "lift/lift.hpp" depending on the robot
#include "claw/claw.hpp"
// #include "intake/intake.hpp"        // uncomment per robot
// #include "tube/tube.hpp"            // uncomment per robot
// #include "pneumatics/pneumatics.hpp" // uncomment per robot

robot-config — the single source of truth for hardware

Every motor, sensor, pneumatic on the robot is defined in robot-config.cpp and declared as extern in robot-config.hpp. When ports change at practice (someone swapped a motor), you update one place.

FILE include/robot-config.hpp extern declarations β€” every other file sees these
#pragma once
#include "api.h"
#include "EZ-Template/api.hpp"

// Drivetrain (managed by EZ-Template)
extern ez::Drive chassis;

// Mechanism motors (raw PROS β€” EZ-Template only handles chassis)
extern pros::Motor arm;         // for Clawbot
// extern pros::Motor lift;     // for Flex
// extern pros::Motor lift_left, lift_right;   // for a 2-motor lift
extern pros::Motor claw;
// extern pros::Motor intake;
// extern pros::Motor tube;

// Sensors (uncomment what's wired)
// extern pros::Optical claw_optical;
// extern pros::Distance back_distance;
// extern pros::adi::DigitalIn arm_top_limit;
// extern pros::adi::Potentiometer arm_pot;

// Pneumatics (if equipped)
// extern pros::adi::DigitalOut tube_cinch;
// extern pros::adi::DigitalOut aligner;
FILE src/robot-config.cpp DEFINITIONS β€” change ports here, nowhere else
#include "main.h"

// Clawbot example β€” replace the values for your robot
ez::Drive chassis(
  {-1},          // Left drive ports (reversed = negative)
  {10},          // Right drive ports
  11,            // IMU port
  4.0,           // Wheel diameter in inches
  200.0,         // Cartridge RPM (200=green, 600=blue)
  1.0            // External gear ratio
);

pros::Motor arm (8, pros::v5::MotorGears::red);
pros::Motor claw(3, pros::v5::MotorGears::red);

// pros::Optical claw_optical(5);
// pros::Distance back_distance(13);
// pros::adi::DigitalIn arm_top_limit('A');
// pros::adi::DigitalIn arm_bot_limit('B');
// pros::adi::Potentiometer arm_pot('C', pros::E_ADI_POT_EDR);

SECTION 5Per-robot subsystem files

Clawbot → Flex → a competition robot →

Each robot's section is collapsed by default. Open one to see its subsystem implementations. Notice that claw.cpp is line-for-line identical between Clawbot and Flex — that's the architecture earning its keep.

CLAWBOT Clawbot — arm + claw 2 subsystems

Two mechanisms: arm (hold-to-move, L1/L2) and claw (rising-edge toggle, R1). Both are raw PROS motors — EZ-Template only manages the chassis.

FILE src/arm/arm.hpp function declarations β€” short and stable
#pragma once
#include "api.h"

void arm_init();
void arm_control(pros::Controller& master);
FILE src/arm/arm.cpp hold-to-move pattern
#include "main.h"

const int ARM_POWER = 80;     // tune here, not in main.cpp

void arm_init() {
  arm.set_brake_mode(pros::v5::MotorBrake::hold);   // doesn't sag under gravity
  arm.tare_position();                              // encoder starts at 0
}

void arm_control(pros::Controller& master) {
  if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L1)) {
    arm.move(ARM_POWER);            // arm up
  } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L2)) {
    arm.move(-ARM_POWER);           // arm down
  } else {
    arm.brake();                    // HOLD position
  }
}
FILE src/claw/claw.hpp function declarations
#pragma once
#include "api.h"

void claw_init();
void claw_control(pros::Controller& master);
FILE src/claw/claw.cpp rising-edge toggle β€” state encapsulated here
#include "main.h"

// File-scope state β€” 'static' keeps these invisible outside this file
static bool claw_open   = false;
static bool last_btn_r1 = false;

void claw_init() {
  claw.set_brake_mode(pros::v5::MotorBrake::hold);
  claw.tare_position();
}

void claw_control(pros::Controller& master) {
  bool r1 = master.get_digital(pros::E_CONTROLLER_DIGITAL_R1);

  // Rising-edge detection: was NOT pressed last loop, IS pressed this loop
  if (r1 && !last_btn_r1) {
    claw_open = !claw_open;
    if (claw_open) claw.move_absolute(200, 50);
    else           claw.move_absolute(0,   50);
  }

  last_btn_r1 = r1;     // remember for next loop
}
Why static matters: the claw_open and last_btn_r1 variables are file-private. If main.cpp accidentally tries to read or write them, the compiler refuses. This is encapsulation enforced by the language itself.
FLEX Flex — lift + claw 2 subsystems · claw.cpp identical to Clawbot

Two mechanisms: lift (hold-to-move, L1/L2) and claw (rising-edge toggle, R1). Same patterns as Clawbot — the claw.cpp file is literally identical (you copy it from the Clawbot project).

FILE src/lift/lift.hpp function declarations
#pragma once
#include "api.h"

void lift_init();
void lift_control(pros::Controller& master);
FILE src/lift/lift.cpp hold-to-move pattern β€” same as Clawbot's arm.cpp, renamed
#include "main.h"

const int LIFT_POWER = 80;

void lift_init() {
  lift.set_brake_mode(pros::v5::MotorBrake::hold);
  lift.tare_position();
}

void lift_control(pros::Controller& master) {
  if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L1)) {
    lift.move(LIFT_POWER);
  } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L2)) {
    lift.move(-LIFT_POWER);
  } else {
    lift.brake();
  }
}
FILE src/claw/claw.cpp IDENTICAL to Clawbot β€” copy-paste
#include "main.h"

static bool claw_open   = false;
static bool last_btn_r1 = false;

void claw_init() {
  claw.set_brake_mode(pros::v5::MotorBrake::hold);
  claw.tare_position();
}

void claw_control(pros::Controller& master) {
  bool r1 = master.get_digital(pros::E_CONTROLLER_DIGITAL_R1);
  if (r1 && !last_btn_r1) {
    claw_open = !claw_open;
    if (claw_open) claw.move_absolute(200, 50);
    else           claw.move_absolute(0,   50);
  }
  last_btn_r1 = r1;
}
This is the architecture's payoff: when a competition robot needs a claw (or some future robot), claw.cpp drops in unchanged. The control code never gets rewritten. Only robot-config.cpp changes per-robot (different motor port).
COMP BOT Competition bot — lift + intake + tube + pneumatics 5 subsystems · competition robot

Five subsystems: lift (2-motor sync, R1/R2), intake (held forward/reverse, L1/L2), tube (non-blocking timed pulses, X/Y), and pneumatics (cinch + aligner toggles, A/B). Each one is its own file.

FILE src/lift/lift.hpp 2-motor lift declarations
#pragma once
#include "api.h"

void lift_init();
void lift_control(pros::Controller& master);
FILE src/lift/lift.cpp 2-motor sync β€” both motors move together
#include "main.h"

const int LIFT_POWER = 110;

void lift_init() {
  lift_right.set_reversed(true);                       // mounted facing left motor
  lift_left.set_brake_mode (pros::v5::MotorBrake::hold);  // doesn't sag
  lift_right.set_brake_mode(pros::v5::MotorBrake::hold);
  lift_left.set_current_limit (2500);                  // 2.5A each = 5A total
  lift_right.set_current_limit(2500);
}

void lift_control(pros::Controller& master) {
  if (master.get_digital(pros::E_CONTROLLER_DIGITAL_R1)) {
    lift_left.move( LIFT_POWER); lift_right.move( LIFT_POWER);
  } else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_R2)) {
    lift_left.move(-LIFT_POWER); lift_right.move(-LIFT_POWER);
  } else {
    lift_left.brake(); lift_right.brake();             // HOLD
  }
}
FILE src/intake/intake.hpp intake declarations
#pragma once
#include "api.h"

void intake_init();
void intake_control(pros::Controller& master);
FILE src/intake/intake.cpp held forward/reverse + jam-protection current limit
#include "main.h"

void intake_init() {
  intake.set_reversed(true);                            // positive = pin-in
  intake.set_brake_mode(pros::v5::MotorBrake::coast);   // spins down naturally
  intake.set_current_limit(2000);                       // 2A β€” jam protection
}

void intake_control(pros::Controller& master) {
  if      (master.get_digital(pros::E_CONTROLLER_DIGITAL_L1)) intake.move( 127);
  else if (master.get_digital(pros::E_CONTROLLER_DIGITAL_L2)) intake.move(-127);
  else                                                        intake.brake();
}
FILE src/tube/tube.hpp tube declarations
#pragma once
#include "api.h"

void tube_init();
void tube_control(pros::Controller& master);
FILE src/tube/tube.cpp non-blocking state machine β€” ALL state encapsulated
#include "main.h"

const int TUBE_PULSE_TICKS = 30;     // 30 * 20ms = 600ms pulse
const int TUBE_POWER       = 80;

// File-scope state β€” invisible to main.cpp
static int tube_timer     = 0;
static int tube_direction = 0;       // -1, 0, or +1

void tube_init() {
  tube.set_brake_mode(pros::v5::MotorBrake::brake);   // snappy stop
  tube.set_current_limit(1500);
}

void tube_control(pros::Controller& master) {
  // Detect button press (rising edge) β€” kick off the state machine
  if (master.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_X)) {
    tube_timer = TUBE_PULSE_TICKS; tube_direction = +1;
  } else if (master.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_Y)) {
    tube_timer = TUBE_PULSE_TICKS; tube_direction = -1;
  }

  // Run motor while counter > 0, brake when it hits 0
  if (tube_timer > 0) {
    tube.move(TUBE_POWER * tube_direction);
    tube_timer--;
  } else {
    tube.brake();
    tube_direction = 0;
  }
}
Why this matters: a naive implementation would do tube.move(80); pros::delay(600); — which freezes the WHOLE opcontrol loop for 600ms. Driver can't drive during the rotation. The state-machine pattern lets everything else (drive, lift, intake, pneumatics) keep running.
FILE src/pneumatics/pneumatics.hpp cinch + aligner declarations
#pragma once
#include "api.h"

void pneumatics_init();
void pneumatics_control(pros::Controller& master);
FILE src/pneumatics/pneumatics.cpp rising-edge toggle pattern β€” both solenoids
#include "main.h"

// File-scope state β€” boot defaults match the physical setup
static bool cinch_open   = true;     // cinch starts OPEN
static bool aligner_open = false;    // aligner starts CLOSED

void pneumatics_init() {
  tube_cinch.set_value(cinch_open);
  aligner.set_value(aligner_open);
}

void pneumatics_control(pros::Controller& master) {
  if (master.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_A)) {
    cinch_open = !cinch_open;
    tube_cinch.set_value(cinch_open);
  }
  if (master.get_digital_new_press(pros::E_CONTROLLER_DIGITAL_B)) {
    aligner_open = !aligner_open;
    aligner.set_value(aligner_open);
  }
}

A competition robot's opcontrol — what main.cpp looks like at the end

FILE src/main.cpp (opcontrol excerpt) 5 subsystems, 6 lines of orchestration
void opcontrol() {
  pros::Controller master(pros::E_CONTROLLER_MASTER);
  while (true) {
    chassis.opcontrol_arcade_standard(ez::SPLIT);   // drive
    lift_control(master);
    intake_control(master);
    tube_control(master);
    pneumatics_control(master);
    pros::delay(20);
  }
}
One glance, full understanding. Anyone reading main.cpp immediately knows: the robot has a lift, intake, tube, and pneumatics. Each one lives in a separate file. Bug in the tube? Open tube.cpp — nothing else to scroll past.

SECTION 6Refactoring from the bringup pattern

If you've already built your code using the Bringup Center pattern (everything in main.cpp), here's how to refactor to subsystem files. Do this only after the code already works. Refactoring broken code is a recipe for losing track of which problem you're solving.

The 5-step refactor

  1. Make sure your bringup code works. All mechanisms respond to buttons correctly. The refactor preserves behavior, so test before and after — the robot should behave identically.
  2. Create the empty subsystem files. For each mechanism, create a folder src/<name>/ containing both src/<name>/<name>.cpp and src/<name>/<name>.hpp. (Headers used to live in include/, but the subsystem-folder layout co-locates each mechanism's two files. Only project-wide headers — main.h, autons.hpp, robot-config.hpp — stay in include/.) Start the new files with just function signatures and empty bodies.
  3. Move one mechanism at a time. Cut the initialization code from initialize() in main.cpp and paste into <name>_init() inside src/<name>/<name>.cpp. Cut the button-handling block from opcontrol() and paste into <name>_control(master). Update main.h to #include "<name>/<name>.hpp" (path-qualified).
  4. Move state variables. Any bool claw_open, int tube_timer, etc. that lived at file scope in main.cpp moves to the subsystem file. Add static to make it file-private.
  5. Build and test. One subsystem at a time. Run after each move — the robot should behave exactly as before. If something breaks, you know which mechanism's refactor introduced the bug.
What stays in main.cpp: only the initialize() orchestration (calling each _init()), the opcontrol() loop (calling each _control()), the autonomous() stub, and the disabled/competition_initialize stubs. Everything mechanism-specific moves out.
Common refactor bug: forgetting to add extern for motor objects in robot-config.hpp. If you get a linker error like "undefined reference to `arm`", that's the cause — the subsystem file can't see the motor object yet.

SECTION 7Reference VS Code projects

Working EZ-Template projects pre-configured in the subsystem-file architecture. Download, extract, open the folder in VS Code with the PROS extension, and you have a baseline to build on.

See the pattern in a full project: the Clawbot and Flex reference builds in the curriculum show this architecture working end-to-end. Open one in VS Code and compare its main.cpp to yours — you’ll see immediately what the refactor produces.

EN4 reminder: reference projects are for studying the architecture, not for turn-in. Your team must write its own version of every subsystem file before competition — the references show the structure, not ready-made answers.

This site is an informational reference. RECF EN4 prohibits AI-generated content in engineering notebooks and programming code. Rewrite everything in your own words.