🤖 Programming · Sensor Fusion · Engineer

Sensor-Fused Clawbot Walkthrough

A complete reference project layering five sensors on top of the V5 Clawbot — distance, AI Vision AprilTags, GPS, potentiometer, and bump switch — all stitched together with EZ-Template. Built to be read, understood, and rewritten in your own words for your engineering notebook.

// Section 01
What This Walkthrough Is
A reference project that takes the trained Clawbot and adds the full Tier 1–6 sensor stack. This guide is the bridge between the single-sensor guides and a full sensor-fused autonomous routine.

The Clawbot Training Platform guide gets the Clawbot driving with EZ-Template + IMU. The Sensor Roadmap tells you the order to add the rest. Each per-sensor guide — distance, switches/pot/GPS, AI Vision/AprilTags — covers one piece in depth.

This guide is the integration story. How do those sensors actually fit together in one project? Where do the function boundaries live? When a sensor disagrees with another sensor, who wins?

📝
RECF EN4 reminder. Code on this page is reference material, the same as every other guide on the site. RECF EN4 prohibits AI-generated content in engineering notebooks and programming code. Read the patterns, understand why each piece is there, then write your own version for your notebook and your competition robot. Do not paste these snippets into either.

What Hardware This Project Assumes

Built on a stock V5 Clawbot with one IMU added (per Clawbot Training), then the following sensors mounted:

PortDevicePurpose
1Left drive motorReversed in chassis ctor (-1)
10Right drive motor
8Arm motorRed 100 RPM cartridge
3Claw motorRed 100 RPM cartridge
11IMUInertial sensor — required by EZ-Template
5Distance sensorFront-facing, low on the chassis
6GPS sensorRear, ~10.5″ off the floor
2AI Vision sensorFront, unobstructed forward view
7Rotation sensorOptional — on an odom tracking wheel
'A' ADIPotentiometer V2Arm angle sensor (top limit)
'H' ADIBump switchArm bottom limit
⚠️
The pot port 'A' here matches the V5 Brain's standard 3-wire ADI labelling. The Sensors-Discrete guide uses 'B' in its examples — the difference is just where you wire it on your robot. Pick one, document it, stick with it.

What Each Section Covers

  1. Project Layout — the file tree and what each file is responsible for. Why split subsystems.hpp from clawbot.hpp.
  2. Arm Dual Limit — the pot+bump pattern. Why one limit isn't enough. The P-controller for preset moves.
  3. Distance + Odom Fusiondrive_to_wall() with two independent exit conditions, noise filter, and optional odometry pose-correction.
  4. AprilTag Sentry — sweep-and-acquire pattern with three exit conditions (distance, tag width, odom safety cap).
  5. GPS Localize — reading the V5 GPS and overwriting EZ-Template's odometry pose only when GPS error is acceptable.
  6. Full Mission — chaining all five subsystems into one autonomous routine. The order matters.
💭
Prerequisites. You should be comfortable with EZ-Template chassis motions, have read the Sensor Roadmap, and ideally have at least one of the per-sensor guides under your belt. This is a Tier 4–6 guide — if you haven't gotten the IMU and basic auton working yet, do that first.
// Section 02
Project Layout — Where Each Piece Lives
A multi-sensor project gets messy fast if you keep dumping everything into main.cpp. The split here matches the EZ-Template project structure but extracts mechanism + sensor logic into its own pair of files.

The File Tree

project/   include/     main.h ............... existing EZ-Template header     subsystems.hpp ........ ports, motors, sensors, controller alias     clawbot.hpp ........... function prototypes for behaviours     autons.hpp ............ autonomous routine prototypes   src/     main.cpp .............. chassis ctor, init, opcontrol, auton selector     clawbot.cpp ........... arm/claw/distance/GPS/sentry implementations     autons.cpp ............ PID constants + the autonomous routines

Why Two Headers Instead of One

subsystems.hpp

The wiring map. Every #define for ports and every device declaration. Change a wire? Edit one line here, the whole project moves with you.

clawbot.hpp

The behaviour API. The functions the rest of the project calls — arm_control(), drive_to_wall(), sentry_acquire(). No port numbers in here.

This split lets you re-port the code to a different robot by editing only subsystems.hpp, without touching the behaviour logic. It's also the simplest example of separation of concerns a programming team can practice.

subsystems.hpp — The Wiring Header

Every device declaration is one line. Every port is a named #define. The Drive object lives in main.cpp (because EZ-Template wants it there); everything else is here.

include/subsystems.hpp
#pragma once #include "EZ-Template/api.hpp" #include "api.h" // ---- Smart ports ---- #define PORT_LEFT_DRIVE 1 #define PORT_RIGHT_DRIVE 10 #define PORT_ARM 8 #define PORT_CLAW 3 #define PORT_IMU 11 #define PORT_DISTANCE 5 #define PORT_GPS 6 #define PORT_AIVISION 2 #define PORT_TRACKER 7 // ---- ADI ports ---- #define ADI_POT 'A' #define ADI_BUMP 'H' extern Drive chassis; // defined in main.cpp // ---- Mechanism motors ---- inline pros::Motor arm (PORT_ARM, pros::E_MOTOR_GEAR_RED); inline pros::Motor claw (PORT_CLAW, pros::E_MOTOR_GEAR_RED); // ---- Sensors ---- inline pros::Distance distance_sensor(PORT_DISTANCE); inline pros::Gps gps (PORT_GPS); inline pros::adi::Potentiometer arm_pot (ADI_POT, pros::E_ADI_POT_V2); inline pros::adi::DigitalIn arm_bump(ADI_BUMP);
💡
Why inline in a header? Without it, including the header from two different .cpp files would create two copies of each device, causing a linker error. The inline keyword tells the compiler "this is the same object across all translation units." Modern C++ (since C++17) supports this for variables — PROS uses C++20.

clawbot.hpp — The Behaviour API

This header is just function prototypes — the verbs the autonomous routines and opcontrol() call. Implementations are in clawbot.cpp.

include/clawbot.hpp (excerpt)
#pragma once #include "main.h" // Tunable arm constants — calibrate per robot. extern int ARM_POT_MIN, ARM_POT_MID, ARM_POT_MAX; // Driver-control behaviours (call once per opcontrol loop) void arm_control(); void claw_control(); // Closed-loop arm move with safety check on both limits void arm_move_to_pot(int target_pot, int max_speed = 60, int timeout_ms = 2000); // Sensor-fused autonomous primitives int drive_to_wall(int target_mm, double max_inches, int speed = 80, bool reset_pose = false, char wall_axis = 'x', double robot_edge_to_centre_in = 7.0, double wall_coord_in = 0.0); void gps_localize(double max_error_m = 0.05, bool verbose = false); bool sentry_search(int target_id, double sweep_deg = 360.0, int speed = 50); void sentry_approach(int target_id, int stop_mm = 200, double max_inches = 60.0);

Notice every public function reads as a sentence: arm control, drive to wall, sentry search. No abbreviations, no internals leaking out. When a teammate reads this header, they instantly know what the project can do.

// Section 03
Arm Dual Limit — Pot for Top, Bump for Bottom
A potentiometer caps how high the arm goes. A bump switch stops the arm before the gearbox grinds against the chassis. Either alone has a failure mode — together they cover both ends of travel.

Why One Limit Isn't Enough

The Switches & Potentiometer guide covers each sensor individually. The pattern below combines them. Here's the failure mode for each-alone:

Pot only (no bump)

Pot reads the angle — but if the pot wire pops loose mid-match it returns a stuck value, the code thinks the arm is fine, and the arm crushes into the bottom hard stop at full power.

Bump only (no pot)

Bump tells you when the arm hits the floor — but says nothing about the top. The arm keeps climbing past its safe range and snaps a chain or stalls the motor.

With both: the pot caps the top, the bump catches the bottom, and a stuck-pot reading still fails safe at the bottom because the bump switch is the final word.

The Control Function

This runs once per opcontrol() loop. Every motor command checks the relevant limit before applying power. Even a held trigger can't drive past either end.

src/clawbot.cpp — arm_control()
static int ARM_POT_SAFE_BAND = 30; static inline bool arm_at_top() { return arm_pot.get_value() >= (ARM_POT_MAX - ARM_POT_SAFE_BAND); } static inline bool arm_at_bottom() { return arm_bump.get_value() == 1 || arm_pot.get_value() <= (ARM_POT_MIN + ARM_POT_SAFE_BAND); } void arm_control() { // Manual triggers bool want_up = master.get_digital(DIGITAL_L1); bool want_down = master.get_digital(DIGITAL_L2); if (want_up && !arm_at_top()) { arm.move_velocity( 60); return; } if (want_down && !arm_at_bottom()) { arm.move_velocity(-50); return; } // Preset buttons (D-pad) if (master.get_digital_new_press(DIGITAL_UP)) { arm_move_to_pot(ARM_POT_MAX, 70, 1500); return; } if (master.get_digital_new_press(DIGITAL_LEFT)) { arm_move_to_pot(ARM_POT_MID, 70, 1500); return; } if (master.get_digital_new_press(DIGITAL_DOWN)) { arm_move_to_pot(ARM_POT_MIN, 70, 1500); return; } // Idle — brake-hold so the arm doesn't sag. arm.brake(); }
⚠️
The safe-band offset matters. ARM_POT_SAFE_BAND = 30 stops the arm 30 raw pot units before the absolute limit. Without this, the arm hits its mechanical hard stop slightly before the pot reading triggers, and the motor stalls against the stop until the loop catches up. The band gives the controller time to react.

The Closed-Loop Move

Preset buttons call arm_move_to_pot() — a small P-controller that drives the arm to a target pot reading. It still respects the bump switch on the way down, so even a "go to MIN" command bails out if the switch fires first.

src/clawbot.cpp — arm_move_to_pot()
void arm_move_to_pot(int target_pot, int max_speed, int timeout_ms) { if (target_pot < ARM_POT_MIN) target_pot = ARM_POT_MIN; if (target_pot > ARM_POT_MAX) target_pot = ARM_POT_MAX; const double kP = 0.25; const int deadband = 15; const int start_ms = pros::millis(); while (pros::millis() - start_ms < timeout_ms) { int reading = arm_pot.get_value(); int error = target_pot - reading; // Hard safety: heading down and bump switch fired? Stop now. if (error < 0 && arm_bump.get_value() == 1) break; // Hard safety: heading up and already at top? Stop now. if (error > 0 && arm_at_top()) break; // Reached target? if (std::abs(error) <= deadband) break; int output = (int)(kP * error); if (output > max_speed) output = max_speed; if (output < -max_speed) output = -max_speed; arm.move_velocity(output); pros::delay(ez::util::DELAY_TIME); } arm.brake(); }

Calibration Workflow

Defaults shipped in the project (ARM_POT_MIN = 1200, MID = 2000, MAX = 2800) are placeholders. Run the calibrate routine, manually move the arm to each useful position, write down the readings, plug them in:

  1. Select "Arm Calibrate" on the auton selector and run the auton (no movement — it just prints the live pot + bump readings to the controller).
  2. Push the arm by hand to its lowest safe position. Note the pot value. That's your ARM_POT_MIN.
  3. Push to a useful mid-height. Note the value. That's your ARM_POT_MID.
  4. Push to the highest safe position. Note the value. That's your ARM_POT_MAX.
  5. Edit the constants at the top of clawbot.cpp and re-upload.
🔭
The pot installation direction matters. Some teams mount the pot so its value goes down as the arm goes up. The code above assumes the pot value increases with arm height. If your pot reads backwards, either flip the mechanical mounting or swap the comparisons in arm_at_top/arm_at_bottom. Check before tuning.
🔬 Check for Understanding
The pot wire pops loose mid-match. arm_pot.get_value() returns a stuck reading of 0. The driver presses L2 to lower the arm. What happens with the dual-limit pattern?
The arm crashes into the bottom hard stop because get_value() == 0 looks like "above MIN".
The arm stops when the bump switch fires, regardless of what the pot reads.
The motor overheats because the loop never exits.
The robot crashes the program because the pot returned an error.
// Section 04
Distance + Odom Fusion — drive_to_wall()
Drive forward until the distance sensor sees a wall. If the distance sensor never confirms, the odometry safety cap stops the robot anyway. Optionally, after stopping, overwrite EZ-Template's odom pose with the now-known coordinates.

Background reading: Distance Sensor Positioning, Distance Sensor Auton Correction, and Sensor-Based Autonomous. The function below combines patterns from all three.

Why Two Exit Conditions, Not One

Either sensor alone has a failure mode:

Combining them: drive using EZ-Template's PID (which handles acceleration and heading-hold), poll the distance sensor each loop, exit on whichever condition fires first.

The Noise Filter

The Sensor-Based Autonomous guide warns: a single under-threshold reading can fire on a wire, an intake roller passing the beam, or a stray reflection. Require two consecutive confirmations before exiting. One bad reading resets the counter.

src/clawbot.cpp — drive_to_wall() (core loop)
int drive_to_wall(int target_mm, double max_inches, int speed, bool reset_pose, char wall_axis, double robot_edge_to_centre_in, double wall_coord_in) { // Hand the chassis PID our travel cap. We'll abort it early via // pid_targets_reset() once the distance sensor confirms. chassis.drive_sensor_reset(); chassis.pid_drive_set(max_inches * okapi::inch, speed); int last_reading = 9999; int confirm = 0; const int CONFIRMS_NEEDED = 2; while (true) { last_reading = distance_sensor.get(); // 9999 = nothing seen // Exit 1: sensor confirmed twice in a row. if (last_reading > 0 && last_reading <= target_mm) { if (++confirm >= CONFIRMS_NEEDED) { chassis.pid_targets_reset(); // abort in-flight PID break; } } else { confirm = 0; // reset on any miss } // Exit 2: odometry safety cap reached. double inches = (chassis.drive_sensor_left() + chassis.drive_sensor_right()) / 2.0; if (std::fabs(inches) >= max_inches) { chassis.pid_targets_reset(); break; } pros::delay(ez::util::DELAY_TIME); } chassis.drive_set(0, 0); chassis.drive_brake_set(MOTOR_BRAKE_HOLD); // (pose reset happens after this — see next code box) return last_reading; }

The Pose-Reset Trick

Once the loop exits and the robot is stopped, you know exactly where the wall is — so you know exactly where the robot is along that axis. Write the corrected coordinate back into EZ-Template's odometry. The next motion runs from a clean reference, with all the drift from earlier moves erased.

src/clawbot.cpp — pose-reset block (continues from above)
if (reset_pose && last_reading > 0 && last_reading <= target_mm) { const double MM_TO_IN = 1.0 / 25.4; double dist_in = last_reading * MM_TO_IN; // Robot-centre offset from the wall = sensor-to-wall + sensor-to-centre. double offset = dist_in + robot_edge_to_centre_in; double new_x = chassis.odom_x_get(); double new_y = chassis.odom_y_get(); double new_t = chassis.odom_theta_get(); if (wall_axis == 'x') { new_x = wall_coord_in - offset; // wall in front -> we're behind it by `offset` } else if (wall_axis == 'y') { new_y = wall_coord_in - offset; } chassis.odom_xyt_set(new_x, new_y, new_t); }
💡
Wall coordinates depend on your origin convention. The example uses field-centred coordinates: a 12′×12′ field has the back wall at x = +72″, the right wall at y = +72″. If you start your auton at a corner with (0, 0) at that corner, the wall coordinates change accordingly. Whichever convention you pick, document it in your engineering notebook and stay consistent across every routine.

Calling It

Two flavours of call: with and without pose reset.

src/autons.cpp — usage examples
// Just stop near the wall — no pose update. drive_to_wall(200, 60.0, DRIVE_SPEED); // Stop near the wall AND reset our X coordinate. // The far wall on a centred 12'x12' field is at x = +72". // Our distance sensor is 7" in front of the robot's centre of rotation. drive_to_wall(/*target_mm=*/200, /*max_inches=*/60.0, DRIVE_SPEED, /*reset_pose=*/true, /*wall_axis=*/'x', /*robot_edge_to_centre_in=*/7.0, /*wall_coord_in=*/72.0);
⚠️
The team's distance-sensor-auton guide references a function called pid_drive_hold_exit_run() — that name doesn't exist in EZ-Template 3.2.x. The actual API is chassis.pid_targets_reset(), used above. Functional outcome is the same: the in-flight PID move aborts cleanly. (If you find the team guide using the older name, it's an aspirational example.)
// Section 05
AprilTag Sentry — Search, Approach, Stop
Spin in place until the AI Vision sensor sees a target AprilTag. Then visual-servo toward it, keeping the tag centred. Three independent stop conditions ensure the robot doesn't run away if any single sensor fails.

Background: AI Vision & AprilTags. The sentry pattern below uses everything that guide describes — the diagonal-corner centre trick, the tag-width-as-distance fallback, and the multi-exit safety pattern.

Search Phase

Spin slowly in place. Each loop, ask the camera "do you see Tag #5?" If yes, stop and return success. If we've turned a full sweep without finding it, return failure.

src/clawbot.cpp — sentry_search()
bool sentry_search(int target_id, double sweep_deg, int speed) { double start_heading = chassis.drive_imu_get(); chassis.drive_set(speed, -speed); // spin in place while (true) { pros::AIVision::Object obj; if (find_tag(target_id, &obj)) { chassis.drive_set(0, 0); chassis.drive_brake_set(MOTOR_BRAKE_HOLD); return true; } if (std::fabs(chassis.drive_imu_get() - start_heading) >= sweep_deg) break; pros::delay(20); } chassis.drive_set(0, 0); return false; }

Approach Phase — Three Exit Conditions

Once we've spotted the tag, drive toward it. Three things can stop us, and that redundancy is the point:

  1. Distance sensor sees the goal/wall close enough.
  2. Tag width in pixels is large enough — the tag has grown big in the camera's view, so we're close.
  3. Odometry safety cap.

The tag-width fallback matters specifically for Override: tags sit on the goal bases, recessed inside the cup-shaped goals, where the distance sensor reads the goal frame, not the tag itself. If the distance sensor stops too early, the tag-width condition still pulls us all the way in.

src/clawbot.cpp — sentry_approach()
void sentry_approach(int target_id, int stop_mm, double max_inches) { const double FRAME_CENTRE = 160.0; // 320px / 2 const double kP_steer = 0.35; const int base_speed = 55; const int STOP_TAG_WIDTH_PX = 200; chassis.drive_sensor_reset(); while (true) { // Exit 3: travel cap. double inches = (chassis.drive_sensor_left() + chassis.drive_sensor_right()) / 2.0; if (std::fabs(inches) >= max_inches) break; // Exit 1: distance-based stop. int dist_mm = distance_sensor.get(); if (dist_mm > 0 && dist_mm <= stop_mm) break; pros::AIVision::Object obj; if (find_tag(target_id, &obj)) { // Diagonal-corner centre per the AprilTags guide. double cx = (obj.object.tag.x0 + obj.object.tag.x2) / 2.0; double err = cx - FRAME_CENTRE; int turn = (int)(kP_steer * err); chassis.drive_set(base_speed + turn, base_speed - turn); // Exit 2: tag is big enough -> we're close enough. int tag_w = std::abs(obj.object.tag.x1 - obj.object.tag.x0); if (tag_w >= STOP_TAG_WIDTH_PX) break; } else { // Lost the tag — crawl rather than full stop. chassis.drive_set(base_speed / 2, base_speed / 2); } pros::delay(20); } chassis.drive_set(0, 0); chassis.drive_brake_set(MOTOR_BRAKE_HOLD); }
💭
Why diagonal corners, not all four? The AprilTags guide recommends (x0+x2)/2 — just the diagonal pair. With a partly-occluded or motion-blurred tag, the four corners can return inconsistent values; the diagonal pair is the most stable centre estimate. Averaging all four sounds more robust but actually amplifies noise from the worst-detected corner.

Tuning STOP_TAG_WIDTH_PX

The 200 px threshold is a placeholder. Find the right value for your robot:

  1. Park the robot at your desired stopping distance from a tagged object.
  2. Read out obj.object.tag.x1 - obj.object.tag.x0 in a debug loop.
  3. That number is your threshold. Plug it in.
⚠️
Enable AprilTag detection mode first. Either through the AI Vision Utility in VEXcode (one-time toggle), or programmatically with aivision.enable_detection_types(pros::AivisionModeType::tags) at startup. Without this, get_all_objects() returns nothing tag-related and your sentry just spins forever. The project's aivision_instance() singleton handles this in code — see clawbot.cpp.
// Section 06
GPS Localize — Trust But Verify
The V5 GPS sensor reads the printed Field Code on the perimeter walls and reports an absolute (X, Y, heading). When it's confident, overwrite EZ-Template's odometry pose. When it's not, leave odometry alone.

Background: V5 GPS Sensor Deep Dive covers the deadzone (~13.5″ from a wall), the field-code mechanics, and the Path A/B/C decision framework. The function below assumes you've already decided GPS is right for your team.

The Confidence Gate

The GPS reports a per-frame RMS error in metres via gps.get_error(). A reading with 0.02 m error is reliable; a reading with 0.30 m error is the GPS struggling to localise from a partial view of the strip. Apply the GPS pose only if error is below your threshold.

src/clawbot.cpp — gps_localize()
void gps_localize(double max_error_m, bool verbose) { pros::gps_status_s_t s = gps.get_position_and_orientation(); double err = gps.get_error(); if (err < 0 || err > max_error_m) { if (verbose) master.print(0, 0, "GPS err %.2fm BAD ", err); return; // trust odom over a bad GPS read } // Metres -> inches for EZ-Template's odom convention. const double M_TO_IN = 39.3701; chassis.odom_xyt_set(s.x * M_TO_IN, s.y * M_TO_IN, s.yaw); if (verbose) master.print(0, 0, "GPS %.1f,%.1f ok ", s.x, s.y); }

When to Call It

GPS is a periodic correction layer, not a primary position source. Call it:

Don't call it inside a fast control loop. The V5 GPS updates at ~10 Hz; calling it every 10 ms wastes time without giving you new data.

The Three Coordinate Systems

This is where teams trip up. There are three coordinate spaces in play:

SystemUnitsOrigin
V5 GPSMetresCentre of field
EZ-Template odomInchesWherever you call odom_xyt_set(0, 0, 0)
Field-coordinate conventionInchesYour team's choice (centre, corner, etc.)

The line chassis.odom_xyt_set(s.x * M_TO_IN, s.y * M_TO_IN, s.yaw) assumes your odom origin matches the GPS origin (centre of field). If you start odom at a corner, you need to add an offset.

🔥
Don't fuse GPS continuously into odometry. A naive "average GPS and odom every loop" looks great in theory and drifts catastrophically in practice. GPS has occasional 5+ centimetre jumps that yank the pose, then odom integrates the wrong velocity. Call gps_localize() as a discrete reset at known-good moments. For continuous fusion, use a Kalman filter — well outside the scope of V5RC and almost never worth the complexity.
⚙ STEM Highlight Sensor Fusion: Discrete Reset vs. Continuous Filtering
There are two main families of sensor fusion algorithms. Continuous filtering (Kalman filter, complementary filter) blends sensor streams every timestep, weighted by each sensor's noise model. Discrete reset trusts a fast but drifty sensor (odometry) for relative motion, periodically replacing its absolute estimate with a slow but unbiased sensor (GPS, AprilTag). The discrete-reset approach is what self-driving cars use for lane-keeping while occasionally re-localising against road landmarks. It's also dramatically simpler to debug, which matters more than theoretical optimality on a 60-second autonomous run.
🎤 Interview line: “We use GPS as a discrete-reset layer, not a continuous filter. Encoder odometry handles fast updates — GPS overrides the pose only when its reported error falls below 5 centimetres. This avoids the failure mode of naive sensor averaging, where a noisy GPS reading injects step errors that the odometry can't recover from.”
// Section 07
Full Mission — Putting It All Together
The auton_full_mission() routine chains every primitive from the previous sections. Read it once for what each step does, then a second time for why each step is in that order.

The Mission

Localise. Drive to a wall and reset pose against it. Turn. Sentry-acquire a tagged object. Grab it. Lift. Drive home. Release.

src/autons.cpp — auton_full_mission()
void auton_full_mission() { // (a) GPS localise — clean pose to start. gps_localize(0.05, true); // (b) Drive to wall + reset pose against it. drive_to_wall(250, 48.0, DRIVE_SPEED, true, 'x', 7.0, 72.0); // (c) Back away, turn toward the tagged object's expected location. chassis.pid_drive_set(-6_in, DRIVE_SPEED); chassis.pid_wait(); chassis.pid_turn_set(-90_deg, TURN_SPEED); chassis.pid_wait(); // (d) Sentry — find Tag #5, drive to it, close claw. sentry_acquire(/*tag=*/5, /*sweep=*/180.0, /*stop_mm=*/150, /*max_inches=*/48.0); arm_move_to_pot(ARM_POT_MIN + 100, 50, 1500); claw_close(); pros::delay(400); arm_move_to_pot(ARM_POT_MAX, 60, 1500); // (e) Drive home. chassis.pid_turn_set(180_deg, TURN_SPEED); chassis.pid_wait(); chassis.pid_drive_set(36_in, DRIVE_SPEED); chassis.pid_wait(); claw_open(); }

Why This Order

Every step is here for a reason. Re-read the code with these annotations:

Selecting It on the Brain

The auton selector registration in main.cpp looks like this:

src/main.cpp — auton selector entries
ez::as::auton_selector.autons_add({ {"Drive To Wall\n\nDistance + odom forward stop", auton_drive_to_wall}, {"Sentry Grab\n\nFind AprilTag, drive to it, grab", auton_sentry_grab}, {"GPS Localize\n\nSquare with GPS pose correction", auton_gps_localize_demo}, {"Full Mission\n\nAll sensors combined", auton_full_mission}, {"Arm Calibrate\n\nPrints pot+bump on controller", auton_arm_calibrate}, });

Five autons cover the full progression: each individual sensor in isolation, then the integrated mission. Tune each one separately before running the mission — debugging a 7-step routine is much harder than debugging one drive-to-wall.

What to Build Next

Stall Detection

Wrap arm_control() with stall-detection so a wedged arm cuts power instead of cooking the motor.

Move Arm Hold to a Task

Use PROS Tasks so the arm holds presets in the background while autonomous keeps driving the chassis.

Refactor Arm to FSM

Turn the arm into a clean finite state machine — IDLE / MOVING / HOLDING / FAULT — once the if-chain grows.

SD-Card Logging

Add SD-card logging to record (time, pot, distance, x, y, theta) per loop. Graph after the run.

📝
One more EN4 reminder. The patterns on this page are reference. The values, ports, and constants are placeholders. Your engineering notebook should describe these patterns in your own words, with screenshots from your robot, your calibration numbers, your design decisions. Judges read for understanding — copy-paste reads as not understanding.
🔬 Check for Understanding
In auton_full_mission(), why is gps_localize() called before drive_to_wall(), even though both can correct the pose?
Because GPS is more accurate than the distance sensor.
GPS gives a free correction if the strip is in view at startup; the distance-sensor reset only works after the robot has driven up to a wall.
The order is arbitrary — either way works.
EZ-Template requires GPS calls to come first.
Related Guides
🪾 Clawbot Training → 🎯 Sensor Roadmap → 📷 AI Vision & AprilTags → 📍 GPS Deep Dive → 🔢 Distance Auton Correction → 🔗 Switches & Pot →
← ALL GUIDES