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

Thresholds depend on mechanism, cartridge, load and command
MeasurementRecord duringReason
Motor velocity percentEmpty running and normal object feedIdentify expected loaded speed
Motor current in ampsStartup and normal feedSeparate normal peaks from sustained trouble
Sensor presenceOne object, gaps and resting objectChoose reliable counting timing
Recovery behaviourShort controlled low-output testVerify 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

Illustrative timing; test your mechanism
StateCommandExit
FORWARDNormal intakeSustained jam evidence
PAUSE BEFORE REVERSEStop100 ms elapsed
REVERSELow-output eject200 ms elapsed
PAUSE AFTER REVERSEStop100 ms elapsed
FORWARDRetry feedNew startup grace period
FAULTStopDeliberate 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:

  1. Require a clear sensor continuously for 60 ms to arm.
  2. Require presence continuously for 50 ms while feeding forward.
  3. Increment once, then disarm.
  4. 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

Do not jam a real mechanism to create a test case
Simulated testExpected result
Presence stays trueOne count after arming
Brief false gapNo new count
Startup object presentNo immediate count
Short low-speed/current spikeNo recovery
Sustained suspect conditionPause then bounded reverse
Repeated confirmed jamsRetry limit then fault
Release during reverseImmediate stop
Mode disabled or BStop
Recovery in counting missionAbort 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

  1. Measure: record velocity/current for empty and normal feeding.
  2. Trace: describe each recovery state and its exit condition.
  3. Simulate: compare a brief startup spike with sustained jam evidence.
  4. Count: test one long presence, two separated objects and a short gap.
  5. Cancel: simulate release during each recovery phase.
  6. Bound: prove that repeated jams reach the retry fault.
  7. Explain: why can a passage count differ from stored inventory?
  8. 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.