VEX V5 PYTHON | 04.3 OPTICAL SENSORS

Guide objective

Read presence, hue and brightness, classify colours carefully and confirm stable detections before using them in autonomous decisions.

01 | PRESENCE AND COLOUR ARE DIFFERENT QUESTIONS

An Optical Sensor can help answer is an object nearby?, what hue does it reflect? and how bright is that reflection?

On a robot, this can support object counting, colour selection or checking that a game piece has reached a known point in an intake.

Detect the object before classifying it. A colour reading without a suitable nearby target should not trigger a confident autonomous decision.

02 | OPTICAL PROXIMITY IS NOT DISTANCE

Choose the reading that fits the task
ReadingWhat it tells youWhat it does not prove
is_near_object()Presence within the sensor’s effective detection conditionsA gap in millimetres or object identity
hue()Colour angle around a colour wheelReliable colour under every lighting condition
brightness()Reflected light level as a percentageDistance or colour by itself
color()A predefined Color classificationYour exact game-piece thresholds

Use a Distance Sensor when the task needs a measured clearance, as in 04.2. Optical proximity is a Boolean, not a substitute for a calibrated distance reading.

Detection depends on the target and mounting. Test the actual working gap rather than assuming that a nearby-object result applies to every material.

03 | MOUNT FOR A CONSISTENT VIEW

Place the sensor where the object crosses its view at a repeatable distance and angle. Keep the lens unobstructed and avoid a view dominated by the chassis or background.

For an intake, guide pieces through a known sensing position. If pieces wobble or pass too quickly, classification may be inconsistent even with correct Python.

optical_sensor = Optical(Ports.PORT13)

Port 13 is an example. Record the actual port. Use a manual declaration or a Devices-panel definition, not both.

04 | CONTROL ILLUMINATION

optical_sensor.set_light(LEDStateType.ON)
optical_sensor.set_light_power(50, PERCENT)

The built-in LED provides local illumination. Keep its power consistent while collecting calibration readings and testing thresholds.

More light is not always better. Reflective surfaces can produce glare, while distance and angle affect the returned light. Compare several settings on your actual objects.

Changing ambient light, sensor position or LED power can change classification. Retest after changing them.

05 | HUE WRAPS AROUND AT RED

Hue is a colour angle. Red sits near both ends of the scale, so a red check often needs two intervals.

is_red = hue <= 20 or hue >= 340
is_blue = 200 <= hue <= 240
Measure your objects before using these example boundaries
Example hueIllustrative classification
5°Red
355°Red
220°Blue
110°Outside these red/blue ranges

These are teaching ranges based on the API’s broad colour categories, not calibrated values for every game piece. Leave room for UNKNOWN instead of forcing every value into red or blue.

The API spells the convenience method color(). For a quick comparison, use optical_sensor.color() == Color.RED. Custom hue ranges give you more control over your measured objects.

06 | BRIGHTNESS NEEDS CONTEXT

Brightness measures reflected light, not how close the object is. A dark surface and a distant surface can both return low brightness.

Hue can be unhelpful for very dark or nearly neutral surfaces. The starter rejects readings outside an example brightness window instead of confidently assigning a colour.

The 10–90% window is a placeholder to calibrate, not a universal validity rule. A bright reading can be legitimate; record evidence before deciding what to reject.

For black, white or grey detection, a different brightness-based strategy and controlled geometry may be appropriate. The red/blue classifier is not designed for those colours.

07 | CONFIRM A STABLE CLASSIFICATION

A brief transition or noisy reading should not immediately trigger a mechanism. Track a candidate label and require it to persist for a short interval.

The diagnostic uses 120 ms as a starting example. UNKNOWN, no object or a different candidate resets that interval.

Repeated polls do not guarantee fresh independent sensor samples. Measure how quickly your object moves through the view and whether the chosen dwell is achievable.

At 0.5 m/s, an object travels 60 mm in 120 ms. If the useful sensing region is shorter, that dwell may reject every moving object. Adjust the mechanism speed, sensing position or confirmation policy based on tests.

08 | COUNT ONCE PER OBJECT VISIT

A loop can read the same red object hundreds of times. Counting once per matching loop would count samples rather than objects.

The example confirms one colour, disarms counting, then waits for sustained absence before rearming. A brief proximity flicker does not immediately create another count.

This counts separate detected visits, not guaranteed unique physical objects. Two touching pieces with no gap may look like one visit; the same piece leaving and returning can count again.

Mechanically separating pieces is often more useful than adding increasingly complicated conditions.

09 | COMPLETE SENSOR-ONLY DIAGNOSTIC

Keep generated imports and Brain setup above this code. Paste below # Begin project code, outside the generated region. Replace earlier movement code for this diagnostic.

The only configured device is an Optical Sensor on the example port 13 plus a Controller. There are no motors or pneumatic commands. Avoid duplicate definitions from the Devices panel.

Present real red and blue objects at the intended mounting gap, then inspect hue, brightness, presence and the count. Calibrate the thresholds before using the result to control a robot.

# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | OPTICAL DIAGNOSTIC
# =============================================================================
# SENSOR CONFIGURATION
# - Optical Sensor: Smart Port 13 (change to your actual port)
# - Controller B cancels the optional bounded wait function.
# - NO motors or pneumatic commands in this diagnostic.
# Keep generated VEX imports and Brain setup above this code.

optical_sensor = Optical(Ports.PORT13)
controller = Controller(PRIMARY)  # Avoid duplicate generated definitions
LIGHT_POWER_PERCENT = 50
POLL_MS = 20
REPORT_MS = 200
STABLE_COLOUR_MS = 120
ABSENCE_RESET_MS = 100
MIN_BRIGHTNESS = 10              # Teaching placeholders: measure your objects
MAX_BRIGHTNESS = 90
RED_LOW_MAX = 20
RED_HIGH_MIN = 340
BLUE_MIN = 200
BLUE_MAX = 240

optical_sensor.set_light(LEDStateType.ON)
optical_sensor.set_light_power(LIGHT_POWER_PERCENT, PERCENT)
wait(200, MSEC)                 # Initial illumination settling pause; test it

def classify_reading(near, hue, brightness):
    if not near:
        return "NO_OBJECT"
    if hue is None or brightness is None:
        return "UNKNOWN"
    if not 0 <= hue < 360 or not 0 <= brightness <= 100:
        return "UNKNOWN"
    if not MIN_BRIGHTNESS <= brightness <= MAX_BRIGHTNESS:
        return "UNKNOWN"
    if hue <= RED_LOW_MAX or hue >= RED_HIGH_MIN:
        return "RED"
    if BLUE_MIN <= hue <= BLUE_MAX:
        return "BLUE"
    return "UNKNOWN"

def read_optical():
    near = optical_sensor.is_near_object()
    if not near:
        return False, None, None, "NO_OBJECT"
    hue = optical_sensor.hue()
    brightness = optical_sensor.brightness()
    # Recheck presence to avoid accepting an object already leaving the view.
    near = optical_sensor.is_near_object()
    return near, hue, brightness, classify_reading(near, hue, brightness)

def wait_for_colour(target, timeout_ms=3000):
    # Sensor-only helper. Caller must handle stopping its own actuators.
    if target not in ("RED", "BLUE") or timeout_ms <= 0:
        raise ValueError("Use RED/BLUE and a positive timeout")
    start_ms = brain.timer.time(MSEC)
    stable_start_ms = None
    while brain.timer.time(MSEC) - start_ms < timeout_ms:
        if controller.buttonB.pressing():
            return "CANCELLED"
        near, hue, brightness, label = read_optical()
        if label == target:
            if stable_start_ms is None:
                stable_start_ms = brain.timer.time(MSEC)
            if brain.timer.time(MSEC) - stable_start_ms >= STABLE_COLOUR_MS:
                return "COLOUR_CONFIRMED"
        else:
            stable_start_ms = None
        wait(POLL_MS, MSEC)
    return "TIMEOUT"

# Continuous diagnostic: count at most once until a sustained absence.
candidate = None
candidate_start_ms = 0
absence_start_ms = None
armed = True
red_count = 0
blue_count = 0
next_report_ms = 0
last_status = "Ready"

while True:
    now_ms = brain.timer.time(MSEC)
    near, hue, brightness, label = read_optical()
    if not near:
        candidate = None
        if absence_start_ms is None:
            absence_start_ms = now_ms
        if now_ms - absence_start_ms >= ABSENCE_RESET_MS:
            armed = True
    else:
        absence_start_ms = None
        if label not in ("RED", "BLUE"):
            candidate = None
        elif label != candidate:
            candidate = label
            candidate_start_ms = now_ms
        elif armed and now_ms - candidate_start_ms >= STABLE_COLOUR_MS:
            if label == "RED":
                red_count += 1
            else:
                blue_count += 1
            armed = False
            last_status = "Confirmed " + label
            print("CONFIRMED", label, red_count, blue_count)

    if now_ms >= next_report_ms:
        brain.screen.clear_screen()
        brain.screen.set_cursor(1, 1)
        brain.screen.print("Presence/label: " + str(near) + "/" + label)
        brain.screen.set_cursor(2, 1)
        brain.screen.print("Hue: " + str(hue) + " Bright: " + str(brightness))
        brain.screen.set_cursor(3, 1)
        brain.screen.print("Red: " + str(red_count) + " Blue: " + str(blue_count))
        brain.screen.set_cursor(4, 1)
        brain.screen.print(last_status)
        print("OPTICAL", near, hue, brightness, label)
        next_report_ms = now_ms + REPORT_MS
    wait(POLL_MS, MSEC)

The main diagnostic runs continuously. The optional wait_for_colour helper is defined but not called by that loop. It demonstrates a bounded confirmation wait and returns COLOUR_CONFIRMED, CANCELLED or TIMEOUT.

No proximity calibration command is required by this example; calibration here means measuring your setup and selecting suitable lighting, thresholds and timing.

10 | A BOUNDED WAIT IS NOT MOTOR CLEANUP

result = wait_for_colour("RED", timeout_ms=3000)
if result == "COLOUR_CONFIRMED":
    print("Red confirmed")
else:
    print("Detection did not complete:", result)

This excerpt belongs in a separate test flow; putting it after the diagnostic’s infinite loop would never execute it.

The helper does not start or stop an intake. If a motor-owning action waits for detection, it must also monitor competition mode, cancellation and its deadline, then stop its actuators on every exit path.

Do not block in a sensor-only helper while assuming other moving mechanisms will stop automatically. Integrate detection into the same monitored loop that owns the motor commands.

11 | EVENTS ARE USEFUL, BUT KEEP CALLBACKS SHORT

object_arrived = False

def on_object_detected():
    global object_arrived
    object_arrived = True

optical_sensor.object_detected(on_object_detected)

The event marks that presence changed. The main control task can then inspect colour and decide what to do. The callback should not contain a long blocking mechanism sequence.

object_lost can flag departure. Presence events do not by themselves provide stable colour classification or solve double counting.

Choose polling or events deliberately. Avoid two independent routines both commanding the same intake.

12 | TEST WITH A CLASSIFICATION LOG

Use several samples and repeated passes, not one reading
Object / conditionPresenceHue rangeBrightness rangeConfirmed labelFalse detections
Red object, normal lightingRecordRecordRecordRecordRecord
Blue object, normal lightingRecordRecordRecordRecordRecord
No targetRecordDo not classifyRecord if usefulNO_OBJECTRecord
Different angle or lightingRecordRecordRecordRecordRecord
Moving object at planned speedRecordRecordRecordRecordRecord

Test glare, target angle, object distance, background and intake speed. Record false positives as well as missed detections.

Choose thresholds that separate the measured objects with a margin. If the measured ranges overlap strongly, improve geometry and illumination before narrowing thresholds arbitrarily.

13 | COMMON MISTAKES

  • Classifying without presence: background readings trigger decisions.
  • Forgetting red wraparound: red near 355° is missed.
  • Counting every poll: one object becomes many counts.
  • Using proximity as millimetres: a Boolean is interpreted as a measured gap.
  • Overlong confirmation: the object leaves before it can be confirmed.
  • Forcing uncertain readings: UNKNOWN becomes an incorrect confident label.
  • Changing illumination without retesting: calibrated thresholds no longer match. Remember when you go to a new competition the venue lighting might change the outcome, even under varing daylight brightness you may need to adjust the values.

CHALLENGES | DETECT AND CLASSIFY RELIABLY

  1. Configure: document sensor port, mounting gap and LED power.
  2. Measure: log hue and brightness ranges for actual red and blue objects.
  3. Explain: distinguish nearby-object detection from distance measurement.
  4. Check wraparound: test hue values near 0° and 359°.
  5. Count: hold one object in view and confirm that it is counted once.
  6. Rearm: remove the object long enough, then present it again.
  7. Evaluate timing: calculate travel during the confirmation interval at your intended object speed.
  8. Integrate: design an intake action with a detection condition, deadline and explicit motor cleanup.
Success evidence

A saved diagnostic, a calibration log, justified thresholds and repeated-pass results. Show one count per detected visit and explain what UNKNOWN, TIMEOUT and CANCELLED mean.

OFFICIAL REFERENCES

The stability and counting logic are team-written examples. This is an independent learning resource; VEX and VEXcode are trademarks of their respective owners.