VEX V5 PYTHON | 05.6 INTAKE RECOVERY
Guide objective
Detect sustained intake trouble, attempt a bounded recovery and count separated object passages without treating uncertain readings as inventory.
01 | A JAM IS A PATTERN, NOT ONE READING
An intake motor can slow briefly while starting or feeding an object. High current can also reflect normal load. Treat one unusual sample as evidence to inspect, not proof of a jam.
The starter detects a suspected jam when a forward command has been active past a startup grace period and low measured speed plus high current persist continuously.
A rotating motor does not guarantee that the object moves: a slipping roller or broken transmission may spin freely. This detector is a useful heuristic, not complete jam recognition.
02 | MEASURE NORMAL FEEDING FIRST
| Measurement | Record during | Reason |
|---|---|---|
| Motor velocity percent | Empty running and normal object feed | Identify expected loaded speed |
| Motor current in amps | Startup and normal feed | Separate normal peaks from sustained trouble |
| Sensor presence | One object, gaps and resting object | Choose reliable counting timing |
| Recovery behaviour | Short controlled low-output test | Verify reversal is mechanically appropriate |
The code uses 10% measured speed and 1.0 A as illustrative thresholds at a 40% forward request. They are not universal limits or promises of protection.
Test all relevant objects and battery/load conditions. Diagnose friction, alignment and roller spacing before automating repeated recovery.
03 | CONFIRM A SUSPECTED JAM OVER TIME
The example ignores jam detection for 250 ms after starting forward. It then requires the combined suspect condition to persist for 200 ms.
If speed recovers or current falls below the threshold, the confirmation timer resets. After a recovery, forward startup receives a fresh grace period.
A missing sensor update or non-finite measurement is a feedback problem, not a normal recovery trigger. The wrapper faults rather than substituting zero for failed feedback.
Timeouts still matter: a broken belt that spins freely may never meet the high-current condition.
04 | USE A BOUNDED RECOVERY SEQUENCE
| State | Command | Exit |
|---|---|---|
| FORWARD | Normal intake | Sustained jam evidence |
| PAUSE BEFORE REVERSE | Stop | 100 ms elapsed |
| REVERSE | Low-output eject | 200 ms elapsed |
| PAUSE AFTER REVERSE | Stop | 100 ms elapsed |
| FORWARD | Retry feed | New startup grace period |
| FAULT | Stop | Deliberate neutral operator reset |
Each continuous forward session permits at most two recovery attempts. A third confirmed jam faults and stops. A six-second run deadline bounds the whole forward/recovery session.
Releasing the command cancels recovery immediately. B and mode changes also stop output. A fault remains latched until an explicit reset.
Reversal is not appropriate for every intake. It can eject an object or move another mechanism. Confirm the physical effect and clearance before enabling it.
05 | COUNT PASSAGES, NOT LOOP ITERATIONS
An object can remain beside an Optical Sensor for many updates. Incrementing a counter whenever is_near_object() is true would count the same object repeatedly.
The counter uses a clear-then-present sequence:
- Require a clear sensor continuously for 60 ms to arm.
- Require presence continuously for 50 ms while feeding forward.
- Increment once, then disarm.
- Wait for a new confirmed clear gap before another count.
An object already present at startup is not counted until it leaves and a new armed passage occurs. Small gaps shorter than the clear interval do not rearm.
Objects packed together without a detectable gap can be undercounted. Placement and object spacing are part of the counting design.
06 | REVERSAL MAKES COUNTING UNCERTAIN
A single presence sensor cannot reliably tell whether an object was acquired, stored, rejected or passed twice. The displayed count is a forward passage count, not guaranteed inventory.
Manual eject or suspected jam recovery marks the count untrusted and disarms the counter. After restarting forward, counting requires another confirmed clear gap, but a returned object can still create a later passage.
The autonomous counting mission stops as soon as trust is lost, before executing a reverse recovery. Driver intake permits bounded recovery but keeps count uncertainty latched until an explicit reset.
For dependable direction and inventory tracking, consider two suitably spaced sensors, a controlled one-way gate or a verified exit sensor. Even then, validate the physical sequence.
07 | COMPLETE SINGLE-MOTOR STARTER
Paste below # Begin project code, outside the generated configuration, keeping VEX imports and Brain setup above it. The example uses an intake motor on Smart Port 7 and Optical Sensor on 13, with no drivetrain commands.
Driver R1 feeds, R2 ejects, B stops and a new X press while R1/R2 are released resets the logic after inspection. MOTION_CONFIGURED begins false.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | SINGLE-MOTOR INTAKE STARTER
# =============================================================================
# Keep generated VEX imports and brain above '# Begin project code'.
# Intake: Smart Port 7, blue cartridge; Optical Sensor: Smart Port 13.
# Verify reversal, feed direction and sensor placement on the actual build.
# Driver R1 intake / R2 eject; B stop. X rising edge resets while neutral.
# No drivetrain commands. Avoid duplicate Devices declarations.
import math
intake_motor = Motor(Ports.PORT7, GearSetting.RATIO_6_1, False)
optical = Optical(Ports.PORT13)
controller = Controller(PRIMARY)
MOTION_CONFIGURED = False
FORWARD_PERCENT = 40
REVERSE_PERCENT = 25
LOW_SPEED_PERCENT = 10 # Measured-speed example, calibrate under normal feed
HIGH_CURRENT_AMPS = 1.0 # Example threshold, not a universal jam/current limit
STARTUP_MS = 250
JAM_CONFIRM_MS = 200
PAUSE_MS = 100
REVERSE_MS = 200
MAX_RECOVERIES = 2
MAX_RUN_MS = 6000
CLEAR_MS = 60
PRESENT_MS = 50
POLL_MS = 20
MAX_LOOP_GAP_MS = 100
AUTO_TARGET_COUNT = 3
# =============================================================================
# PURE LOGIC: NO VEX API CALLS IN THESE CLASSES
# =============================================================================
class PassageCounter:
def __init__(self):
self.count = 0
self.armed = False
self.clear_since = None
self.present_since = None
def disarm(self):
self.armed = False
self.clear_since = self.present_since = None
def update(self, near, forward, now):
if not forward:
self.disarm()
return
if not near:
self.present_since = None
if self.clear_since is None:
self.clear_since = now
if now - self.clear_since >= CLEAR_MS:
self.armed = True
else:
self.clear_since = None
if not self.armed:
return
if self.present_since is None:
self.present_since = now
if now - self.present_since >= PRESENT_MS:
self.count += 1
self.armed = False
self.present_since = None
class IntakeLogic:
def __init__(self):
self.state = "IDLE"
self.counter = PassageCounter()
self.count_trusted = True
self.attempts = 0
self.started = self.phase_started = self.forward_started = 0
self.suspect_since = None
def fault(self, reason):
self.state = "FAULT: " + reason
self.count_trusted = False
self.counter.disarm()
return 0
def step(self, request, speed, current, near, now, active=True):
# request: +1 intake, -1 manual eject, 0 stop.
if not active:
self.counter.disarm()
if not self.state.startswith("FAULT"):
self.state = "IDLE"
return 0
if self.state.startswith("FAULT"):
return 0 # Reset only through an explicit neutral operator action.
if request == 0:
self.counter.disarm()
self.state = "IDLE"
self.suspect_since = None
return 0
if request < 0:
self.count_trusted = False
self.counter.disarm()
self.state = "EJECT"
return -REVERSE_PERCENT
if self.state in ("IDLE", "EJECT"):
self.state = "FORWARD"
self.started = self.forward_started = now
self.suspect_since = None
self.attempts = 0
if now - self.started >= MAX_RUN_MS:
return self.fault("RUN_TIMEOUT")
if self.state == "FORWARD":
self.counter.update(near, True, now)
suspect = (now - self.forward_started >= STARTUP_MS
and speed < LOW_SPEED_PERCENT
and current >= HIGH_CURRENT_AMPS)
if suspect:
if self.suspect_since is None:
self.suspect_since = now
if now - self.suspect_since >= JAM_CONFIRM_MS:
self.count_trusted = False
self.counter.disarm()
if self.attempts >= MAX_RECOVERIES:
return self.fault("RETRIES_EXHAUSTED")
self.attempts += 1
self.state = "PAUSE_BEFORE_REVERSE"
self.phase_started = now
return 0
else:
self.suspect_since = None
return FORWARD_PERCENT
self.counter.disarm()
if self.state == "PAUSE_BEFORE_REVERSE":
if now - self.phase_started >= PAUSE_MS:
self.state = "REVERSE"
self.phase_started = now
return -REVERSE_PERCENT
return 0
if self.state == "REVERSE":
if now - self.phase_started >= REVERSE_MS:
self.state = "PAUSE_AFTER_REVERSE"
self.phase_started = now
return 0
return -REVERSE_PERCENT
if self.state == "PAUSE_AFTER_REVERSE":
if now - self.phase_started >= PAUSE_MS:
self.state = "FORWARD"
self.forward_started = now
self.suspect_since = None
return FORWARD_PERCENT
return 0
return self.fault("UNKNOWN_STATE")
# =============================================================================
# HARDWARE WRAPPER: SINGLE OUTPUT OWNER
# =============================================================================
def stop_intake():
intake_motor.stop(BRAKE)
def apply_output(output):
if output == 0:
stop_intake()
else:
intake_motor.spin(FORWARD if output > 0 else REVERSE,
abs(output), PERCENT)
def mode_active(mode):
return (competition.is_enabled() and
(competition.is_autonomous() if mode == "AUTO"
else competition.is_driver_control()))
def update_hardware(logic, request, now):
speed = abs(intake_motor.velocity(PERCENT))
current = intake_motor.current(CurrentUnits.AMP)
near = optical.is_near_object()
if not math.isfinite(speed) or not math.isfinite(current):
return logic.fault("INVALID_FEEDBACK")
if not MOTION_CONFIGURED or controller.buttonB.pressing():
return logic.step(0, speed, current, near, now, active=False)
return logic.step(request, speed, current, near, now)
def autonomous():
logic = IntakeLogic() # Counter starts unarmed; existing object not counted.
started = previous = brain.timer.time(MSEC)
result = "NOT_CONFIGURED"
try:
if not MOTION_CONFIGURED:
return
while mode_active("AUTO"):
now = brain.timer.time(MSEC)
if controller.buttonB.pressing():
result = "CANCELLED"
break
if now - previous > MAX_LOOP_GAP_MS:
result = "LOOP_TIMING_FAULT"
break
previous = now
output = update_hardware(logic, 1, now)
# Counting mission aborts before reversing an uncertain object.
if not logic.count_trusted:
result = "COUNT_UNCERTAIN: " + logic.state
break
if logic.counter.count >= AUTO_TARGET_COUNT:
result = "PASSAGE_TARGET_REACHED"
break
if now - started >= MAX_RUN_MS:
result = "TIMEOUT"
break
apply_output(output)
wait(POLL_MS, MSEC)
else:
result = "MODE_CHANGED"
except Exception:
result = "READ_OR_COMMAND_FAULT"
finally:
stop_intake()
print(result, "passages", logic.counter.count)
def driver_control():
logic = IntakeLogic()
previous_x = controller.buttonX.pressing()
previous = brain.timer.time(MSEC)
try:
while mode_active("DRIVER"):
now = brain.timer.time(MSEC)
if now - previous > MAX_LOOP_GAP_MS:
logic.fault("LOOP_TIMING_FAULT")
previous = now
forward = controller.buttonR1.pressing()
reverse = controller.buttonR2.pressing()
request = int(forward) - int(reverse)
x = controller.buttonX.pressing()
if x and not previous_x and not forward and not reverse:
# Deliberate reset after inspection; count restarts at zero.
stop_intake()
logic = IntakeLogic()
previous_x = x
output = update_hardware(logic, request, now)
apply_output(output)
wait(POLL_MS, MSEC)
except Exception:
print("INTAKE READ_OR_COMMAND_FAULT")
finally:
stop_intake()
print(logic.state, "passages", logic.counter.count,
"trusted", logic.count_trusted)
stop_intake()
optical.set_light(LEDStateType.ON)
optical.set_light_power(50, PERCENT)
competition = Competition(driver_control, autonomous)
while True:
wait(100, MSEC)
The state-machine step never waits for a recovery phase to finish. It returns the output for the current update, allowing cancellation to be checked on each loop.
Manual R2 eject is a held operator command; the six-second deadline applies to forward and automatic recovery sessions. Driver counting resets between sessions only through X or a fresh mode callback.
For two-stage intakes, measure each motor separately. A group reading may represent one motor and hide the other stage’s jam. Coordinate the stages under one subsystem owner, rather than adding this single-motor logic unchanged.
08 | DISTINGUISH A COUNT TARGET FROM DELIVERY
PASSAGE_TARGET_REACHED means the configured number of qualified forward detections occurred. It does not prove the last object reached storage or the scoring mechanism.
The example stops on the count target. If your system needs a finishing feed action, add a bounded, verified transfer step with appropriate downstream feedback.
A short presence interval can miss fast objects; a long interval can merge close objects. A 20 ms polling loop also limits what events it can observe. Choose timing using measured transit speed and sensor placement.
09 | TEST LOGIC BEFORE PHYSICAL RECOVERY
| Simulated test | Expected result |
|---|---|
| Presence stays true | One count after arming |
| Brief false gap | No new count |
| Startup object present | No immediate count |
| Short low-speed/current spike | No recovery |
| Sustained suspect condition | Pause then bounded reverse |
| Repeated confirmed jams | Retry limit then fault |
| Release during reverse | Immediate stop |
| Mode disabled or B | Stop |
| Recovery in counting mission | Abort with uncertainty |
First test the pure logic classes with scripted readings. Then test sensor placement and normal feeding physically at conservative output.
Use supported, clear-space tests to verify reversal. Investigate a real jam with power stopped rather than repeatedly retrying against a trapped object.
10 | LOG AND IMPROVE THE MECHANISM
Record command, measured velocity, current, state, retry count, presence and count trust. Log transitions or use a lower logging rate so printing does not delay the control loop.
Separate false detections from actual feed failures. Adjust one threshold or timing setting at a time, then repeat the same object sequence.
Frequent recovery is evidence to review roller geometry, object guides, spacing and friction. Recovery should handle occasional disruption, not compensate indefinitely for an unreliable build.
Ordinary exceptions and callback exits stop through finally. Forced task termination needs deliberate mode-transition testing and a single motor-output owner.
CHALLENGES | DETECT, RECOVER AND COUNT
- Measure: record velocity/current for empty and normal feeding.
- Trace: describe each recovery state and its exit condition.
- Simulate: compare a brief startup spike with sustained jam evidence.
- Count: test one long presence, two separated objects and a short gap.
- Cancel: simulate release during each recovery phase.
- Bound: prove that repeated jams reach the retry fault.
- Explain: why can a passage count differ from stored inventory?
- Extend: propose a second sensor or verified transfer step and explain what it resolves.
Success evidence
A labelled setup, measured thresholds, simulated-state tests and normal-feed log. Identify count uncertainty explicitly and document when the operator should stop, inspect and reset.
OFFICIAL REFERENCES
The detection thresholds, recovery policy and counting logic are independently written teaching examples. VEX and VEXcode are trademarks of their respective owners.