VEX V5 PYTHON | 09.3 SENSOR CONDITIONS
Guide objective
Turn raw sensor readings into clear conditions using debouncing, filtering and hysteresis, while keeping immediate stop checks responsive.
01 | FROM A READING TO A DECISION
A Distance Sensor reports 301, 298, 303 and 299 mm. A single threshold at 300 mm would keep changing its answer. A switch can also briefly alternate between pressed and released when its contacts bounce.
A sensor condition is a named Boolean decision based on evidence. For example: object visible, wall nearby, lift at its lower stop, or target centred.
Keep three ideas separate: whether the reading is valid, what it measures, and whether it satisfies the condition needed by the current action.
02 | CHOOSE THE RIGHT TOOL
| Tool | Purpose | Example |
|---|---|---|
| Debouncing | Accept a Boolean change after it persists | A switch stays pressed for 60 ms |
| Median filter | Reduce isolated numeric spikes | Middle of the last five ranges |
| Exponential smoothing | Reduce rapid numeric fluctuation | Move partway toward each new measurement |
| Hysteresis | Use different enter and exit thresholds | Near at 300 mm; leave at 340 mm |
| Dwell / qualification | Require a condition for a duration | Target window remains valid for 200 ms |
| Edge detection | Act once on a transition | Count a new object once when absent becomes present |
Do not automatically stack every filter. Each extra stage adds state and can add delay. Start with recorded raw data, then choose the simplest rule that fixes the observed problem.
03 | DEBOUNCE A SWITCH WITHOUT BLOCKING
Track a candidate state and the time it first appeared. Restart that timer whenever the raw state changes. Accept the candidate only after it lasts long enough.
| Time (ms) | Raw pressed | Result with 60 ms debounce |
|---|---|---|
| 0 | False | Stable False |
| 20 | True | Candidate True starts |
| 40 | False | Candidate resets |
| 60 | True | New candidate True |
| 100 | True | Still only 40 ms |
| 120 | True | Stable True; one rising edge |
The DebouncedBool class in the complete program returns the stable state plus rising and falling edges. Holding the switch pressed does not produce another rising edge.
A 60 ms setting is illustrative. Measure the actual switch and task. At a 20 ms poll interval, acceptance is quantised to observed samples; long gaps do not prove the signal stayed unchanged.
04 | DO NOT DELAY A LIMIT STOP
For a mechanism moving toward a physical stop, the first raw pressed reading should inhibit further movement in that direction. Debounced confirmation can identify a stable home position afterwards.
Do not continue pushing into a stop while waiting for a debounce timer. After a stop, use a deliberate rearm or homing policy rather than automatically restarting when contacts bounce.
The complete approach example treats its raw contact input as a fault immediately. Its debounced switch value is recorded only for observation. This is not a complete lift-homing controller.
05 | MEDIAN FILTER: REMOVE AN ISOLATED SPIKE
| Raw window (mm) | Sorted window | Median |
|---|---|---|
| 500, 502, 90, 501, 499 | 90, 499, 500, 501, 502 | 500 |
| 500, 500, 250, 250, 250 | 250, 250, 250, 500, 500 | 250 |
A median selects the middle value after sorting. It can reject one unusual sample without averaging that spike into the result. It cannot reliably reject a sustained bad reading or several wrong values.
Here, the filter waits for five valid samples before producing a value. Invalid data empties the window. Old values must not act as current evidence after a dropout.
A short raw reading may represent a real obstacle. The starter checks the raw close-range guard before filtering, even when the median would reject that reading.
06 | EXPONENTIAL SMOOTHING AND ITS DELAY
An exponential moving average (EMA) updates a stored value using:
filtered = filtered + alpha * (new_value - filtered)
If filtered is 500 mm, the new value is 450 mm and alpha is 0.4, the result is 480 mm. A smaller alpha smooths more but follows real changes more slowly. Alpha 1 uses the new value directly.
The starter feeds a five-sample median into an EMA with alpha 0.4. This combination is a teaching example, not a default for every robot. Compare median only, EMA only and the combined result on the same logged data.
Alpha assumes a reasonably regular update period. Changing the poll interval changes the effective response. For variable intervals, a time-based coefficient such as 1 - exp(-dt / tau) can preserve a chosen time constant.
07 | HYSTERESIS PREVENTS THRESHOLD CHATTER
Use one threshold to enter a state and another to leave it. In the gap between the two, keep the previous state.
| Reading | Previous NEAR | New NEAR |
|---|---|---|
| 299 mm | False | True: enter at or below 300 |
| 315 mm | True | True: remain near |
| 315 mm | False | False: not yet near |
| 340 mm | True | False: exit at or above 340 |
Hysteresis adds no fixed waiting period, but it does add memory. Reset that memory when the evidence becomes unavailable. This rule classifies proximity; it does not prove an exact position.
08 | BUILD CONDITIONS WITH AND, OR AND NOT
# All required evidence must be valid before declaring success.
can_finish = range_valid and target_near and mechanism_ready
# Any urgent guard can stop the action.
must_stop = raw_contact or raw_too_close or cancelled
# Missing evidence is a separate outcome, not a far-away object.
if not range_valid:
stop_drive()Use names that explain the meaning and units: range_valid, target_near, heading_in_tolerance. Keep unavailable data distinct from False. Otherwise, an unplugged or non-detecting sensor can accidentally look like a clear path.
For object counting, use a qualified absent-to-present edge, then require a qualified absence before rearming. Sensor placement and object spacing determine whether the edge represents one real object.
09 | COMPLETE NON-BLOCKING SENSOR APPROACH
This forward-only practice routine starts stationary, fills a valid range window, approaches slowly, then brakes when filtered proximity enters the near state. It confirms the observed window while keeping motors stopped.
Paste below # Begin project code without editing the generated configuration region. Keep its VEX imports and Brain declaration. Manual configuration: left motors 3/4, right motors 1/2, Distance Sensor 16 and Limit Switch on Brain three-wire A. Do not duplicate these in Devices.
MOTION_CONFIGURED starts False. Verify motor signs, sensor alignment, bumper setback and a clear test lane. Thresholds are measured from the sensor face. The 120 mm raw guard and 8% request are illustrative, not a validated stopping margin for your robot.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | SENSOR CONDITIONS AND FILTERING
# =============================================================================
# Keep generated imports and Brain setup above '# Begin project code'.
# Manual declarations below: do not duplicate the same devices in Devices.
import math
left_1 = Motor(Ports.PORT3, GearSetting.RATIO_6_1, False)
left_2 = Motor(Ports.PORT4, GearSetting.RATIO_6_1, False)
right_1 = Motor(Ports.PORT1, GearSetting.RATIO_6_1, True)
right_2 = Motor(Ports.PORT2, GearSetting.RATIO_6_1, True)
left_drive = MotorGroup(left_1, left_2)
right_drive = MotorGroup(right_1, right_2)
front_distance = Distance(Ports.PORT16)
contact = Limit(brain.three_wire_port.a)
controller = Controller(PRIMARY)
MOTION_CONFIGURED = False # Verify direction, sensor mounting and stopping distance.
POLL_MS = 20
MAX_GAP_MS = 100
ROUTE_TIMEOUT_MS = 8000
ACQUIRE_TIMEOUT_MS = 1000
MEDIAN_SAMPLES = 5
EMA_ALPHA = 0.4
NEAR_ENTER_MM = 300.0 # From the sensor face, not the front bumper.
NEAR_EXIT_MM = 340.0
CONFIRM_MAX_MM = 320.0
RAW_STOP_MM = 120.0 # Illustrative guard: choose from actual geometry/speed.
CONFIRM_MS = 200
SWITCH_DEBOUNCE_MS = 60
APPROACH_PERCENT = 8.0
def stop_drive():
left_drive.stop(BRAKE)
right_drive.stop(BRAKE)
def apply_forward(value):
if value == 0:
stop_drive()
else:
left_drive.spin(FORWARD, value, PERCENT)
right_drive.spin(FORWARD, value, PERCENT)
class DebouncedBool:
def __init__(self, hold_ms, initial=False):
self.hold_ms = hold_ms
self.value = initial
self.candidate = initial
self.since = None
def update(self, now, raw):
raw = bool(raw)
if self.since is None or raw != self.candidate:
self.candidate, self.since = raw, now
rising = falling = False
if raw != self.value and now-self.since >= self.hold_ms:
old = self.value
self.value = raw
rising = self.value and not old
falling = old and not self.value
return self.value, rising, falling
class RangeFilter:
def __init__(self, size, alpha):
if size < 1 or size % 2 == 0 or not 0 < alpha <= 1:
raise ValueError('Need odd positive size and 0 < alpha <= 1')
self.size, self.alpha = size, alpha
self.values = []
self.filtered = None
def reset(self):
self.values = []
self.filtered = None
def update(self, raw):
if raw is None or not math.isfinite(raw) or not 20 <= raw <= 2000:
self.reset()
return None
self.values.append(raw)
if len(self.values) > self.size:
self.values.pop(0)
if len(self.values) < self.size:
return None
median = sorted(self.values)[self.size//2]
if self.filtered is None:
self.filtered = median
else:
self.filtered += self.alpha*(median-self.filtered)
return self.filtered
class NearCondition:
def __init__(self, enter, leave):
if enter >= leave:
raise ValueError('Enter must be below leave')
self.enter, self.leave, self.value = enter, leave, False
def update(self, value):
if value is None:
self.value = False
elif value <= self.enter:
self.value = True
elif value >= self.leave:
self.value = False
return self.value
class SensorApproach:
def __init__(self, now):
self.started = now
self.state, self.reason = 'ACQUIRE', ''
self.filter = RangeFilter(MEDIAN_SAMPLES, EMA_ALPHA)
self.near = NearCondition(NEAR_ENTER_MM, NEAR_EXIT_MM)
self.switch = DebouncedBool(SWITCH_DEBOUNCE_MS)
self.confirm_since = None
self.filtered = None
self.switch_state = False
def fail(self, reason):
self.state, self.reason = 'FAULT', reason
return 0.0
def update(self, now, raw, raw_contact):
if self.state in ('DONE_PROXIMITY', 'FAULT'):
return 0.0
self.switch_state, rising, falling = self.switch.update(now, raw_contact)
# Debounced state is for observation, NOT the contact stop decision.
if raw_contact:
return self.fail('RAW_CONTACT')
if now-self.started >= ROUTE_TIMEOUT_MS:
return self.fail('ROUTE_TIMEOUT')
if raw is None or not math.isfinite(raw) or not 20 <= raw <= 2000:
self.filter.reset()
self.near.update(None)
self.confirm_since = None
self.filtered = None
return self.fail('RANGE_UNAVAILABLE')
# A short raw reading bypasses the median and EMA.
if raw <= RAW_STOP_MM:
return self.fail('RAW_TOO_CLOSE')
self.filtered = self.filter.update(raw)
near = self.near.update(self.filtered)
if self.state == 'ACQUIRE':
if now-self.started >= ACQUIRE_TIMEOUT_MS:
return self.fail('ACQUIRE_TIMEOUT')
if self.filtered is not None:
self.state = 'CONFIRM' if near else 'APPROACH'
return 0.0 # Do not move until the filter has a full valid window.
if self.state == 'APPROACH':
if near:
self.state = 'CONFIRM'
self.confirm_since = None
return 0.0
return APPROACH_PERCENT
if self.state == 'CONFIRM':
# Brake throughout confirmation. No automatic restart or reverse.
if not near or raw > CONFIRM_MAX_MM or self.filtered > CONFIRM_MAX_MM:
return self.fail('CONFIRM_WINDOW_LOST')
if self.confirm_since is None:
self.confirm_since = now
if now-self.confirm_since >= CONFIRM_MS:
self.state = 'DONE_PROXIMITY'
return 0.0
return self.fail('UNKNOWN_STATE')
def cancelled():
return (not competition.is_enabled() or not competition.is_autonomous()
or controller.buttonB.pressing())
def read_inputs():
pressed = contact.pressing()
raw = None
if front_distance.is_object_detected():
raw = front_distance.object_distance(MM)
return raw, pressed
def run_sensor_approach():
machine = SensorApproach(brain.timer.time(MSEC))
last_tick = None
log_at = -1000
try:
while True:
before = brain.timer.time(MSEC)
if cancelled():
return 'CANCELLED'
raw, pressed = read_inputs()
now = brain.timer.time(MSEC)
if cancelled():
return 'CANCELLED'
if now-before > MAX_GAP_MS or (last_tick is not None
and now-last_tick > MAX_GAP_MS):
return 'UPDATE_GAP'
last_tick = now
old = machine.state
output = machine.update(now, raw, pressed)
if cancelled():
return 'CANCELLED'
apply_forward(output)
if old != machine.state or now-log_at >= 200:
print(now, machine.state, 'raw', raw, 'filtered', machine.filtered,
'switch', machine.switch_state, 'output', output, machine.reason)
log_at = now
if machine.state in ('DONE_PROXIMITY', 'FAULT'):
return machine.state+': '+machine.reason
wait(POLL_MS, MSEC)
except Exception:
return 'SENSOR_OR_OUTPUT_FAULT'
finally:
stop_drive()
def autonomous():
stop_drive()
if MOTION_CONFIGURED:
print('sensor approach result', run_sensor_approach())
else:
print('Set MOTION_CONFIGURED only after verifying the test setup')
def driver_control():
stop_drive() # Teaching project: no driver movement.
competition = Competition(driver_control, autonomous)
stop_drive()
while True:
wait(100, MSEC)
The routine expects a fixed, sufficiently detectable surface already in view. Loss of detection faults immediately. There is no automatic search, restart or reverse. It does not hold an inertial heading; use a straight, aligned test lane before adding heading control from 05.4.
DONE_PROXIMITY means a valid proximity window persisted while zero motion was requested. It does not prove physical contact, an exact field pose or that the chassis has fully stopped. Add independent low-motion evidence if the next action requires verified settling.
10 | UNDERSTAND THE PRIORITY ORDER
| Priority | Evidence | Response |
|---|---|---|
| 1 | B, mode change, excessive update gap | Exit and brake |
| 2 | Raw contact, unavailable range, raw too close | FAULT; zero output |
| 3 | Median window and EMA | Update normal proximity evidence |
| 4 | Near classification | Brake before confirmation |
| 5 | Valid confirmation window for 200 ms | DONE_PROXIMITY; keep output zero |
Near enters at 300 mm, exits at 340 mm, and confirmation also requires both raw and filtered readings at or below 320 mm. These are deliberately different rules: classification has a wider band, while completion has a tighter observation window.
If confirmation loses its window, the example faults rather than moving again. A real controller may choose another policy, but it must be explicit.
11 | LOG RESPONSE, NOT JUST SMOOTHNESS
Record timestamp, raw value, median, EMA, condition state, action state and requested output. Plot them together or inspect a small table around every transition.
- Stationary surface: measure normal variation.
- Single injected numeric spike: compare the filters.
- A real step change: measure the lag to the new condition.
- Slow approach: measure stop range and repeatability.
- Missing detection: verify immediate zero output.
- Raw contact and short range: verify they bypass smoothing.
- Mode cancellation and delayed updates: verify cleanup.
At 20 ms polling, a five-sample window takes about 80 ms from first to fifth sample under ideal scheduling. Median response, EMA response, API latency, polling, braking and chassis inertia all contribute to how far the robot travels before stopping. Measure the complete chain.
12 | COMMON MISTAKES
- Using wait() inside a condition: keep timers as stored timestamps.
- Recreating the filter each tick: retain its history for the action.
- Keeping old samples through dropouts: invalidate and reset.
- Counting a held signal every cycle: use a qualified edge and rearm rule.
- Smoothing an urgent stop: keep a raw guard path.
- Treating no object as a clear path: distinguish missing evidence.
- Filtering until the trace looks perfect: measure response delay.
- Calling proximity proof of contact: name the outcome accurately.
CHALLENGES | CONDITION, FILTER AND VERIFY
- Trace: calculate stable states and rising edges for a bouncing switch.
- Compare: pass the same range sequence through median only, EMA only and both.
- Measure: halve alpha and compare step-response delay.
- Classify: explain the state at 315 mm before and after entering NEAR.
- Invalidate: inject None and prove that no previous filtered value is reused.
- Bypass: inject a raw reading of 100 mm into a window near 500 mm and verify zero output.
- Qualify: add a present/absent object counter that rearms after a stable absence.
- Test: measure repeated stop ranges at two conservative speeds and justify your thresholds.
Success evidence
Raw and processed traces, edge counts, response timings, dropout and guard tests, and repeated physical stop measurements. Explain what your completion state proves and what it cannot prove.
OFFICIAL REFERENCES
- Distance Sensor detection and measurement API
- Limit Switch pressing and configuration API
- Competition mode and callback API
The filtering classes and state machine are independently written teaching examples. Validate thresholds, timing and physical behaviour on your own setup.