Subsystem-File Architecture for VEXcode V5
Per-mechanism .cpp files so main.cpp stays as a thin orchestration layer. Same architectural principles as the PROS reference, written for the VEXcode V5 C++ toolchain.
SECTION 1Why subsystem files
Once you have 3+ mechanisms, main.cpp becomes a 300-line monster: drive logic, lift logic, claw logic, intake logic all jammed into one usercontrol() loop. 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.
- Hard to read — usercontrol has 80 lines of mixed button-handling for every mechanism. Finding "where does the tube move" requires scrolling.
- Hard to debug — when the claw misbehaves you can't comment out the claw code without surgically extracting it.
- Hard to hand off — two students editing
main.cppat the same time always produces merge conflicts. - Hard to reuse — claw logic on Clawbot is identical to claw logic on Flex, but it's intertwined with everything else.
usercontrol() function in main.cpp should fit in 10 lines or less, regardless of how many mechanisms the robot has. If it doesn't, mechanism logic is still in main — refactor it out.
SECTION 2The file structure
Every Spartan VEXcode project follows this layout. Files marked NEW are the ones we add beyond what VEXcode's "new project" wizard gives you.
src/ and include/ flat. The encapsulation still works through file-scope static, the <name>_init() naming convention, and one extern-driven robot-config file.
.h extension, not .hpp (which PROS used). The vex.h master header is auto-generated by VEXcode and pulls in the entire VEX framework. Your own headers (arm.h, claw.h, etc.) follow the same convention.
SECTION 3The four convention rules
Stick to these rules 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() | 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 scope | Compiler-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 file | Tuning 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 defined | Single source of truth for port assignments. All other files use extern declarations via robot-config.h. |
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 inside usercontrol().
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.
#include "vex.h"
#include "robot-config.h"
#include "autons.h"
#include "arm.h" // or "lift.h" depending on the robot
#include "claw.h"
// #include "intake.h" // uncomment per robot
// #include "tube.h" // uncomment per robot
// #include "pneumatics.h" // uncomment per robot
using namespace vex;
competition Competition;
// ═══════════════════════════════════════════════════════════════
// PRE-AUTON — runs once at program start, before any phase
// ═══════════════════════════════════════════════════════════════
void pre_auton() {
Brain.Screen.print("Spartan Design");
Brain.Screen.newLine();
Brain.Screen.print("Initializing...");
// Calibrate the inertial sensor (~3 seconds)
Inertial.calibrate();
while (Inertial.isCalibrating()) wait(50, msec);
// Per-subsystem init — one call per mechanism
arm_init(); // or lift_init() depending on robot
claw_init();
// intake_init();
// tube_init();
// pneumatics_init();
Brain.Screen.clearScreen();
Brain.Screen.setCursor(1, 1);
Brain.Screen.print("Ready.");
}
// ═══════════════════════════════════════════════════════════════
// AUTONOMOUS — runs during 15s auton period
// ═══════════════════════════════════════════════════════════════
void autonomous() {
// For a real season, replace with selector logic (see Section 8)
auton_drive_test();
}
// ═══════════════════════════════════════════════════════════════
// USERCONTROL — driver-control loop
//
// THE 10-LINE TEST: count the lines inside this while-loop. For a
// 2-mechanism robot, it's 4 lines. Adding a 3rd mechanism adds 1 line.
// ═══════════════════════════════════════════════════════════════
void usercontrol() {
while (true) {
// Drive — split arcade (left stick forward/back, right stick turning)
int forward_pct = Controller1.Axis3.position();
int turn_pct = Controller1.Axis1.position();
LeftDrive.spin(fwd, forward_pct + turn_pct, percent);
RightDrive.spin(fwd, forward_pct - turn_pct, percent);
// Mechanisms — one line per subsystem
arm_control();
claw_control();
// intake_control();
// tube_control();
// pneumatics_control();
wait(20, msec);
}
}
// ═══════════════════════════════════════════════════════════════
// MAIN — VEXcode entry point. Don't touch unless you know why.
// ═══════════════════════════════════════════════════════════════
int main() {
Competition.autonomous(autonomous);
Competition.drivercontrol(usercontrol);
pre_auton();
while (true) wait(100, msec); // keep the main thread alive
}
robot-config — the single source of truth for hardware
Every motor, sensor, and pneumatic on the robot is defined in robot-config.cpp and declared as extern in robot-config.h. When ports change at practice, you update one file.
#pragma once #include "vex.h" // ─── Brain + Controller ────────────────────────────────────────── extern vex::brain Brain; extern vex::controller Controller1; // ─── Drivetrain motors ─────────────────────────────────────────── // We control drive motors directly via spin() — simple and explicit. // (Alternative: vex::drivetrain class for built-in auton drive routines.) extern vex::motor LeftDriveA; extern vex::motor RightDriveA; // extern vex::motor LeftDriveB; // uncomment for 4-motor drive // extern vex::motor RightDriveB; // extern vex::motor_group LeftDrive; // extern vex::motor_group RightDrive; // ─── Inertial sensor (heading) ─────────────────────────────────── extern vex::inertial Inertial; // ─── Mechanism motors ──────────────────────────────────────────── extern vex::motor Arm; // Clawbot — hold-to-move L1/L2 extern vex::motor Claw; // Clawbot/Flex — rising-edge toggle R1 // extern vex::motor Lift; // Flex — hold-to-move L1/L2 // ─── Sensors (uncomment as wired) ──────────────────────────────── // extern vex::limit arm_top_limit; // extern vex::limit arm_bot_limit; // extern vex::optical claw_optical; // extern vex::distance back_distance; // ─── Pneumatics (if equipped) ─────────────────────────────────── // extern vex::digital_out tube_cinch; // extern vex::digital_out aligner;
#include "vex.h" #include "robot-config.h" using namespace vex; // ─── Brain + Controller ────────────────────────────────────────── brain Brain; controller Controller1; // ─── Drivetrain motors ─────────────────────────────────────────── // Clawbot example — 2-motor tank. Adjust ports + gearset for your bot. // // Constructor: motor(port, gearset, reverse) // ratio6_1 = blue cartridge (600 RPM) // ratio18_1 = green cartridge (200 RPM) // ratio36_1 = red cartridge (100 RPM) // motor LeftDriveA (PORT1, ratio18_1, true); // reversed (mounted backwards) motor RightDriveA (PORT10, ratio18_1, false); // For 4-motor drive, uncomment these + the externs: // motor LeftDriveB (PORT2, ratio6_1, true); // motor RightDriveB (PORT9, ratio6_1, false); // motor_group LeftDrive (LeftDriveA, LeftDriveB); // motor_group RightDrive(RightDriveA, RightDriveB); // ─── Inertial sensor ───────────────────────────────────────────── inertial Inertial(PORT11); // ─── Mechanism motors ──────────────────────────────────────────── motor Arm (PORT8, ratio36_1, false); // 100 RPM red — torque for lifting motor Claw (PORT3, ratio36_1, false); // 100 RPM red — fine grip control // ─── Sensors (uncomment as wired) ──────────────────────────────── // limit arm_top_limit (Brain.ThreeWirePort.A); // limit arm_bot_limit (Brain.ThreeWirePort.B); // optical claw_optical (PORT5); // distance back_distance (PORT13); // ─── Pneumatics (if equipped) ─────────────────────────────────── // digital_out tube_cinch (Brain.ThreeWirePort.A); // digital_out aligner (Brain.ThreeWirePort.B);
motor constructor encodes both the cartridge AND the gear ratio. ratio6_1 = 6:1 internal gearing = blue cartridge (600 RPM). ratio18_1 = green (200 RPM). ratio36_1 = red (100 RPM). External gear reduction (e.g. a 36:48 on drive) isn't encoded here — just use the cartridge speed and let your auton code handle distance calculations.SECTION 5Per-robot subsystem files
Each robot's section is collapsed by default. Notice that claw.cpp is line-for-line identical between Clawbot and Flex — that's the architecture earning its keep.
CLAWBOT Clawbot — arm + claw
Two mechanisms: arm (hold-to-move, L1/L2) and claw (rising-edge toggle, R1).
#pragma once void arm_init(); void arm_control(); // Auton API — let autons.cpp drive the arm without touching motors directly void arm_set_power(int power); // -100..100 (percent) void arm_stop();
#include "vex.h"
#include "robot-config.h"
#include "arm.h"
using namespace vex;
// ─── Tunable constants (top of file = easy to find) ───────────────
const int ARM_POWER = 80; // 0–100 percent; reduce for finer control
// ─── Subsystem init (called once from pre_auton()) ───────────────
void arm_init() {
Arm.setStopping(hold); // doesn't sag under gravity
Arm.setPosition(0, degrees); // encoder starts at 0
}
// ─── Opcontrol — called every usercontrol loop ───────────────────
void arm_control() {
if (Controller1.ButtonL1.pressing()) {
Arm.spin(forward, ARM_POWER, percent);
} else if (Controller1.ButtonL2.pressing()) {
Arm.spin(reverse, ARM_POWER, percent);
} else {
Arm.stop(); // HOLD position (set by setStopping above)
}
}
// ─── Auton API ─────────────────────────────────────────────────────
void arm_set_power(int power) {
if (power >= 0) Arm.spin(forward, power, percent);
else Arm.spin(reverse, -power, percent);
}
void arm_stop() {
Arm.stop();
}
#pragma once void claw_init(); void claw_control(); // Auton API void claw_open_set(); void claw_close_set(); bool claw_is_open();
#include "vex.h"
#include "robot-config.h"
#include "claw.h"
using namespace vex;
// ─── Tunable constants ─────────────────────────────────────────────
const int CLAW_CLOSED_DEG = 0;
const int CLAW_OPEN_DEG = 200; // adjust for your claw geometry
const int CLAW_SPEED = 50; // 0–100 percent
// ─── File-scope state (static = invisible outside this file) ──────
static bool claw_open = false; // is the claw currently OPEN?
static bool last_btn_r1 = false; // R1 state in previous loop
// ─── Subsystem init ────────────────────────────────────────────────
void claw_init() {
Claw.setStopping(hold);
Claw.setPosition(0, degrees);
}
// ─── Opcontrol — rising-edge detection ────────────────────────────
void claw_control() {
bool r1 = Controller1.ButtonR1.pressing();
// Rising-edge: NOT pressed last loop, IS pressed this loop
if (r1 && !last_btn_r1) {
claw_open = !claw_open;
if (claw_open) Claw.spinToPosition(CLAW_OPEN_DEG, degrees, CLAW_SPEED, percent, false);
else Claw.spinToPosition(CLAW_CLOSED_DEG, degrees, CLAW_SPEED, percent, false);
}
last_btn_r1 = r1; // remember for next loop
}
// ─── Auton API ─────────────────────────────────────────────────────
void claw_open_set() {
claw_open = true;
Claw.spinToPosition(CLAW_OPEN_DEG, degrees, CLAW_SPEED, percent, false);
}
void claw_close_set() {
claw_open = false;
Claw.spinToPosition(CLAW_CLOSED_DEG, degrees, CLAW_SPEED, percent, false);
}
bool claw_is_open() { return claw_open; }
static matters: claw_open and last_btn_r1 are file-private. main.cpp can't accidentally read or modify them — the compiler refuses. The accessor claw_is_open() exposes a read-only view if other code (like the brain dashboard) needs to know the state.spinToPosition() last argument: the final false means "don't wait for the move to finish before returning." This is critical in claw_control() — if it were true, the claw would block usercontrol() for the duration of the move and the driver couldn't drive while the claw was opening.FLEX Flex — lift + claw
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).
#pragma once void lift_init(); void lift_control(); void lift_set_power(int power); void lift_stop();
#include "vex.h"
#include "robot-config.h"
#include "lift.h"
using namespace vex;
const int LIFT_POWER = 80;
void lift_init() {
Lift.setStopping(hold);
Lift.setPosition(0, degrees);
}
void lift_control() {
if (Controller1.ButtonL1.pressing()) {
Lift.spin(forward, LIFT_POWER, percent);
} else if (Controller1.ButtonL2.pressing()) {
Lift.spin(reverse, LIFT_POWER, percent);
} else {
Lift.stop();
}
}
void lift_set_power(int power) {
if (power >= 0) Lift.spin(forward, power, percent);
else Lift.spin(reverse, -power, percent);
}
void lift_stop() {
Lift.stop();
}
#include "vex.h"
#include "robot-config.h"
#include "claw.h"
using namespace vex;
const int CLAW_CLOSED_DEG = 0;
const int CLAW_OPEN_DEG = 200;
const int CLAW_SPEED = 50;
static bool claw_open = false;
static bool last_btn_r1 = false;
void claw_init() {
Claw.setStopping(hold);
Claw.setPosition(0, degrees);
}
void claw_control() {
bool r1 = Controller1.ButtonR1.pressing();
if (r1 && !last_btn_r1) {
claw_open = !claw_open;
if (claw_open) Claw.spinToPosition(CLAW_OPEN_DEG, degrees, CLAW_SPEED, percent, false);
else Claw.spinToPosition(CLAW_CLOSED_DEG, degrees, CLAW_SPEED, percent, false);
}
last_btn_r1 = r1;
}
void claw_open_set() { claw_open = true; Claw.spinToPosition(CLAW_OPEN_DEG, degrees, CLAW_SPEED, percent, false); }
void claw_close_set() { claw_open = false; Claw.spinToPosition(CLAW_CLOSED_DEG, degrees, CLAW_SPEED, percent, false); }
bool claw_is_open() { return claw_open; }
claw.cpp drops in unchanged. Only robot-config.cpp changes per-robot (different motor port). The control code never gets rewritten.COMP BOT Competition bot — lift + intake + tube + pneumatics
Four 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).
#pragma once void lift_init(); void lift_control(); void lift_set_power(int power); void lift_stop();
#include "vex.h"
#include "robot-config.h"
#include "lift.h"
using namespace vex;
const int LIFT_POWER = 90; // 0–100 percent
void lift_init() {
// LiftMotors is a motor_group defined in robot-config.cpp.
// Both motors share the same brake mode and respond to one command.
LiftMotors.setStopping(hold);
LiftMotors.setMaxTorque(50, percent); // ~5A total = 2.5A per motor
}
void lift_control() {
if (Controller1.ButtonR1.pressing()) {
LiftMotors.spin(forward, LIFT_POWER, percent);
} else if (Controller1.ButtonR2.pressing()) {
LiftMotors.spin(reverse, LIFT_POWER, percent);
} else {
LiftMotors.stop();
}
}
void lift_set_power(int power) {
if (power >= 0) LiftMotors.spin(forward, power, percent);
else LiftMotors.spin(reverse, -power, percent);
}
void lift_stop() { LiftMotors.stop(); }
motor_group: simpler than driving two motors manually. motor_group LiftMotors(lift_left, lift_right); — one object, one command, both motors spin together. Reversal is set on the individual motors in their constructors. This is one place VEXcode is genuinely cleaner than PROS.#pragma once void intake_init(); void intake_control(); void intake_set_power(int power); void intake_stop();
#include "vex.h"
#include "robot-config.h"
#include "intake.h"
using namespace vex;
void intake_init() {
Intake.setStopping(coast); // spins down naturally
Intake.setMaxTorque(40, percent); // ~2A — jam protection
}
void intake_control() {
if (Controller1.ButtonL1.pressing()) Intake.spin(forward, 100, percent);
else if (Controller1.ButtonL2.pressing()) Intake.spin(reverse, 100, percent);
else Intake.stop();
}
void intake_set_power(int power) {
if (power >= 0) Intake.spin(forward, power, percent);
else Intake.spin(reverse, -power, percent);
}
void intake_stop() { Intake.stop(); }
#pragma once void tube_init(); void tube_control(); // Auton: kick off a pulse (non-blocking — returns immediately, the state // machine in tube_control() advances the rotation over the next ~600ms). // Only useful in auton if tube_control() is being ticked. For linear auton // sequences, use tube_pulse_blocking() instead. void tube_pulse(int direction); void tube_pulse_blocking(int direction);
#include "vex.h"
#include "robot-config.h"
#include "tube.h"
using namespace vex;
// ─── Tunable constants ─────────────────────────────────────────────
const int TUBE_PULSE_TICKS = 30; // 30 × 20ms = 600ms pulse
const int TUBE_POWER = 80; // 0–100 percent
// ─── File-scope state — invisible to main.cpp ─────────────────────
static int tube_timer = 0;
static int tube_direction = 0; // -1, 0, or +1
static bool prev_x = false;
static bool prev_y = false;
void tube_init() {
Tube.setStopping(brake); // snappy stop
Tube.setMaxTorque(30, percent); // ~1.5A — gentle
}
void tube_control() {
// Detect button press (rising edge) — kick off the state machine
bool x = Controller1.ButtonX.pressing();
bool y = Controller1.ButtonY.pressing();
if (x && !prev_x) { tube_timer = TUBE_PULSE_TICKS; tube_direction = +1; }
if (y && !prev_y) { tube_timer = TUBE_PULSE_TICKS; tube_direction = -1; }
prev_x = x;
prev_y = y;
// Run motor while counter > 0, brake when it hits 0
if (tube_timer > 0) {
if (tube_direction > 0) Tube.spin(forward, TUBE_POWER, percent);
else Tube.spin(reverse, TUBE_POWER, percent);
tube_timer--;
} else {
Tube.stop();
tube_direction = 0;
}
}
// Auton: non-blocking variant — only useful if tube_control() runs
void tube_pulse(int direction) {
tube_timer = TUBE_PULSE_TICKS;
tube_direction = direction;
}
// Auton: blocking variant — runs synchronously and returns when done
void tube_pulse_blocking(int direction) {
if (direction > 0) Tube.spin(forward, TUBE_POWER, percent);
else Tube.spin(reverse, TUBE_POWER, percent);
wait(TUBE_PULSE_TICKS * 20, msec); // 30 ticks × 20ms = 600ms
Tube.stop();
}
Tube.spin(...); wait(600, msec); — which freezes the WHOLE usercontrol loop for 600ms. Driver can't drive during the rotation. The state-machine pattern lets everything else (drive, lift, intake, pneumatics) keep running while the tube is pulsing.#pragma once void pneumatics_init(); void pneumatics_control(); // Auton API — explicit setters (not toggles, so autons can request specific state) void cinch_set(bool open); void aligner_set(bool open); bool cinch_is_open(); bool aligner_is_open();
#include "vex.h"
#include "robot-config.h"
#include "pneumatics.h"
using namespace vex;
// ─── File-scope state — boot defaults match physical setup ────────
static bool cinch_open = true; // cinch starts OPEN
static bool aligner_open = false; // aligner starts CLOSED
static bool prev_a = false;
static bool prev_b = false;
void pneumatics_init() {
TubeCinch.set(cinch_open);
Aligner.set(aligner_open);
}
void pneumatics_control() {
bool a = Controller1.ButtonA.pressing();
bool b = Controller1.ButtonB.pressing();
if (a && !prev_a) {
cinch_open = !cinch_open;
TubeCinch.set(cinch_open);
}
if (b && !prev_b) {
aligner_open = !aligner_open;
Aligner.set(aligner_open);
}
prev_a = a;
prev_b = b;
}
// Auton API
void cinch_set(bool open) { cinch_open = open; TubeCinch.set(cinch_open); }
void aligner_set(bool open) { aligner_open = open; Aligner.set(aligner_open); }
bool cinch_is_open() { return cinch_open; }
bool aligner_is_open() { return aligner_open; }
A competition robot's usercontrol — what main.cpp looks like at the end
void usercontrol() {
while (true) {
// Drive — split arcade
int forward_pct = Controller1.Axis3.position();
int turn_pct = Controller1.Axis1.position();
LeftDrive.spin(forward, forward_pct + turn_pct, percent);
RightDrive.spin(forward, forward_pct - turn_pct, percent);
// Mechanisms — one line per subsystem
lift_control();
intake_control();
tube_control();
pneumatics_control();
wait(20, msec);
}
}
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 6Autons — using the subsystem auton API
Autons live in autons.cpp. They call subsystem auton APIs (arm_set_power(), claw_open_set(), lift_stop(), etc.) rather than touching motor objects directly. This keeps the architecture clean: each subsystem owns its hardware.
#pragma once void auton_drive_test(); void auton_arm_cycle(); void auton_grab_and_score();
#include "vex.h"
#include "robot-config.h"
#include "autons.h"
#include "arm.h"
#include "claw.h"
using namespace vex;
// ─── Drive helpers (since we drive motors manually, not vex::drivetrain) ──
// For a more featureful drivetrain class, see VEXcode's vex::drivetrain
// — it has built-in driveFor(distance) and turnFor(angle) using the IMU.
static void drive_for_ms(int power, int duration_ms) {
LeftDriveA.spin(forward, power, percent);
RightDriveA.spin(forward, power, percent);
wait(duration_ms, msec);
LeftDriveA.stop();
RightDriveA.stop();
}
static void turn_for_degrees(int target_heading_deg, int power) {
double start = Inertial.heading();
double goal = start + target_heading_deg;
while (true) {
double err = goal - Inertial.heading();
if (std::abs(err) < 2.0) break;
int sign = (err > 0) ? 1 : -1;
LeftDriveA.spin(forward, sign * power, percent);
RightDriveA.spin(reverse, sign * power, percent);
wait(20, msec);
}
LeftDriveA.stop();
RightDriveA.stop();
}
// ═══════════════════════════════════════════════════════════════════
// 1. DRIVE TEST — short forward + back to verify motor wiring + IMU
// ═══════════════════════════════════════════════════════════════════
void auton_drive_test() {
drive_for_ms( 60, 1200); // forward 60% for 1.2s
wait(300, msec);
drive_for_ms(-60, 1200); // back 60% for 1.2s
}
// ═══════════════════════════════════════════════════════════════════
// 2. ARM CYCLE — mechanism-in-auton validation (no driving)
// ═══════════════════════════════════════════════════════════════════
void auton_arm_cycle() {
arm_set_power(80);
wait(1000, msec);
arm_stop();
wait(500, msec);
arm_set_power(-50);
wait(1000, msec);
arm_stop();
}
// ═══════════════════════════════════════════════════════════════════
// 3. GRAB AND SCORE — composite drive + claw + arm sequence
// ═══════════════════════════════════════════════════════════════════
void auton_grab_and_score() {
claw_open_set();
wait(400, msec);
drive_for_ms(70, 900); // approach (~900ms forward)
claw_close_set();
wait(400, msec);
arm_set_power(80);
wait(800, msec);
arm_stop();
drive_for_ms(-70, 700); // carry away
claw_open_set();
wait(400, msec);
arm_set_power(-50);
wait(800, msec);
arm_stop();
}
vex::drivetrain: VEXcode provides a built-in drivetrain class with driveFor(forward, 24, inches) and turnFor(right, 90, degrees) methods that handle IMU correction automatically. It's simpler than the manual helpers above. We use manual helpers here because they're explicit about what's happening — great for learning. For competition autons, the reference projects use vex::drivetrain instead.SECTION 7Refactoring from monolithic main.cpp
If you have working VEXcode code that puts everything in one big usercontrol() function, 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
- Make sure your code works. All mechanisms respond to buttons correctly. The refactor preserves behavior — test before and after, the robot should behave identically.
- Create the empty subsystem files. For each mechanism, create
src/<name>.cppandinclude/<name>.h. Start with function signatures and empty bodies. - Move one mechanism at a time. Cut the initialization code from
pre_auton()inmain.cppand paste into<name>_init(). Cut the button-handling block fromusercontrol()and paste into<name>_control(). Updatemain.cppto#include "<name>.h". - Move state variables. Any
bool claw_open,int tube_timer, etc. that lived at file scope inmain.cppmoves to the subsystem file. Addstaticto make it file-private. - 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.
pre_auton() orchestration (calling each _init()), the usercontrol() loop (calling each _control()), the autonomous() dispatch, and main(). Everything mechanism-specific moves out.
extern for motor objects in robot-config.h. 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 8Auton selector — DIY (since VEXcode doesn't ship one)
Unlike EZ-Template, VEXcode doesn't include a built-in auton selector. Build your own. The pattern below uses controller buttons (UP/DOWN to cycle, A to confirm) and the brain screen for display — ~30 lines of code and a great teaching moment about state and event handling.
// ─── Auton selector — add near the top of main.cpp ────────────
// Each entry: name + function pointer
struct AutonEntry {
const char* name;
void (*fn)();
};
AutonEntry autons[] = {
{"1. Drive Test", auton_drive_test},
{"2. Arm Cycle", auton_arm_cycle},
{"3. Grab and Score", auton_grab_and_score},
};
const int n_autons = sizeof(autons) / sizeof(autons[0]);
int selected_auton = 0;
void render_selector() {
Brain.Screen.clearScreen();
Brain.Screen.setCursor(1, 1);
Brain.Screen.print("=== Auton Selector ===");
Brain.Screen.newLine();
for (int i = 0; i < n_autons; i++) {
Brain.Screen.print(" %s %s", (i == selected_auton) ? ">" : " ", autons[i].name);
Brain.Screen.newLine();
}
Brain.Screen.newLine();
Brain.Screen.print("Up/Down to cycle A to confirm");
}
// Append to pre_auton(), after subsystem inits:
void run_selector() {
render_selector();
while (!Controller1.ButtonA.pressing()) {
if (Controller1.ButtonUp.pressing()) {
selected_auton = (selected_auton + n_autons - 1) % n_autons;
render_selector();
wait(250, msec); // debounce
}
if (Controller1.ButtonDown.pressing()) {
selected_auton = (selected_auton + 1) % n_autons;
render_selector();
wait(250, msec);
}
wait(50, msec);
}
Brain.Screen.clearScreen();
Brain.Screen.print("Selected: %s", autons[selected_auton].name);
}
// Replace autonomous() with:
void autonomous() {
autons[selected_auton].fn();
}
run_selector(): add it at the end of pre_auton(), after all _init() calls. The selector blocks until the driver presses A — by design. The reference projects include this selector pre-wired.SECTION 9Reference VEXcode projects
Working VEXcode V5 C++ projects pre-configured in the subsystem-file architecture. Two editions ship side-by-side:
Single-file edition (recommended) — for the VEXcode V5 standalone IDE (Blocks + Text). Architecture preserved as banner-commented sections inside one main.cpp. Setup is 5 minutes: paste one file, build, download.
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.