VEX V5 PYTHON | 10.2 TELEMETRY AND TEST LOGS
Guide objective
Choose useful measurements, display their age, tune bounded settings while disabled and capture structured samples and events without flooding the control loop.
01 | TELEMETRY IS EVIDENCE FROM THE RUN
Telemetry is measured or computed information reported while a system operates. For an autonomous robot, useful telemetry explains what it was trying to do, what it observed and what it requested next.
A screen answers “what is happening now?” A test log answers “what happened, when and under which settings?” Both need timestamps and clear labels.
Keep the run’s physical outcome separate from a controller’s internal success flag. Reaching an encoder target does not always mean reaching the intended field position.
02 | LOG THE VALUES THAT EXPLAIN THE ACTION
| Category | Useful fields | Reason |
|---|---|---|
| Identity | Run ID, code/config version, route ID | Compare like with like |
| Target and feedback | Target distance, measured travel, heading or pose | See the error the controller sees |
| Control | Error, requested output, output cap | Explain correction and saturation |
| State and outcome | Action state, transition, cancellation or fault reason | Locate when the route changed |
| Timing | Elapsed time, loop gap, computation time | Identify delayed updates |
| Quality | Validity, age, gate result | Avoid mistaking stale or rejected evidence for current data |
| Conditions | Battery, temperatures, surface and load | Explain repeatability changes from 09.6 |
Label requests as requests. A requested 12% is not a measured speed, a motor voltage or proof that the chassis moved.
03 | USE THREE DIFFERENT UPDATE RATES
| Work | Illustrative rate | Policy |
|---|---|---|
| Control updates | Every 20 ms | Compute from current feedback and keep cancellation responsive |
| Sample records | Every 100 ms | Store a small bounded record |
| Brain display | Every 200 ms | Draw the latest published snapshot |
| Console or file export | After the run, while disabled | Perform slow I/O outside motion control |
Do not print every controller iteration or rewrite a large file while moving. The work can delay the next control update.
A separate display task still shares platform resources. Measure the effect on loop gaps and keep one screen writer. A low refresh rate reduces demand but does not prove zero overhead.
04 | SEPARATE SAMPLES FROM EVENTS
Samples are periodic observations. Events are meaningful changes such as START, ENTER_SETTLING, LOST_TARGET, CANCELLED or RESULT.
A 100 ms sample schedule can miss a short fault transition. Record the event when the controller observes it, even between scheduled samples.
sample: time, state, target, measured, error, request event: time, transition_or_outcome, reason
The starter has separate bounded buffers for samples and events. It preserves the first sample window and keeps recent events, including the final result. Drop counts are exported so missing records are visible.
A finite buffer is not a complete black-box recorder. Select capacity from run length, sampling rate and measured memory use.
05 | TUNE ONLY WHILE DISABLED
Provide bounded plus/minus controls for a small number of parameters. Start with a proportional gain and request cap. Use units: proportional gain in this example is percent request per millimetre of error.
Pending settings belong to the interface. At run entry, capture a fixed configuration tuple for that run. Editing the next run’s settings must not alter the active controller.
Record the configuration version and actual parameter values in every row. After a change, repeat the same test rather than changing several values and guessing which helped.
The demonstration has no moving robot. Its permitted gain and cap ranges are teaching choices, not proven limits for your drivetrain.
06 | SHOW AGE AND DISTINGUISH FINAL DATA
The display reads a published immutable snapshot. It shows the state, target, measured value, error, request, run settings and milliseconds since publication.
LIVE means a run is active, not that the sample has zero age. FINAL means the run ended; values remain available for review rather than being presented as new measurements.
For real sensors, maintain a separate validity and acquisition-age policy. Publication time alone does not prove the sensor produced a new reading. Show NA or an invalid label when evidence is unavailable.
07 | COMPLETE TUNING AND TELEMETRY DEMO
This standalone project uses a software model only. It commands no motors or sensors. Paste below # Begin project code, keeping generated VEX imports and Brain setup. Use a project with no other movement code.
While disabled, use KP-/KP+ and CAP-/CAP+ to adjust pending settings. Enter autonomous through your competition test setup to run the software demonstration. B, disable or a mode change cancels it.
The toy model assumes each percent request produces 10 mm/s without inertia, slip, load or sensor noise. It cannot predict actual VEX motion or supply real tuning gains.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | TELEMETRY AND TUNING DEMO
# =============================================================================
# Keep generated VEX imports and Brain setup above '# Begin project code'.
# SOFTWARE MODEL ONLY: no motors, sensors or real route are commanded.
import math
CODE_ID = 'telemetry_demo_v1'
TARGET_MM = 200.0
POLL_MS = 20
LOG_MS = 100
SCREEN_MS = 200
MAX_GAP_MS = 100
RUN_LIMIT_MS = 6000
SAMPLE_CAPACITY = 100
EVENT_CAPACITY = 20
controller = Controller(PRIMARY)
# Immutable pending config: (version, kp percent/mm, request cap percent).
pending = (1,0.10,12.0)
active = False
run_number = 0
published = None # Immutable (Brain timestamp, record tuple).
message = 'DEMO: NO MOTOR OUTPUT'
HEADER = ('code_id','run_id','kind','t_ms','state','target_mm','measured_mm',
'error_mm','request_pct','kp_pct_per_mm','cap_pct','gap_ms',
'cycle_ms','valid','reason')
class LogBuffer:
def __init__(self):
self.samples=[]
self.events=[]
self.dropped_samples=0
self.dropped_events=0
self.exported=True
def add(self,record,event=False):
if event:
if len(self.events)>=EVENT_CAPACITY:
self.events.pop(0)
self.dropped_events+=1
self.events.append(record) # Recent terminal events are retained.
else:
if len(self.samples)>=SAMPLE_CAPACITY:
self.dropped_samples+=1
else:
self.samples.append(record) # Preserve the first bounded window.
self.exported=False
def records(self):
return sorted(self.samples+self.events,key=lambda r:r[3])
logs = LogBuffer()
def make_record(run,config,kind,t,state,measured,request,gap,cycle,reason=''):
valid = (measured is not None and all(math.isfinite(v)
for v in (measured,request,gap,cycle)))
return (CODE_ID+'_cfg'+str(config[0]),run,kind,t,state,TARGET_MM,
measured if valid else None,TARGET_MM-measured if valid else None,
request if valid else None,config[1],config[2],gap,cycle,
1 if valid else 0,reason)
def publish(record,event=False):
global published
logs.add(record,event)
published=(brain.timer.time(MSEC),record)
def csv_cell(value):
if value is None:return 'NA'
text=str(round(value,3) if isinstance(value,float) else value)
if any(c in text for c in (',','"','\n','\r')):
return '"'+text.replace('"','""')+'"'
return text
def export_csv():
global message
if active or competition.is_enabled():return
rows=logs.records() # Copy after the producer has finished.
print(','.join(HEADER))
for row in rows:
if competition.is_enabled():
message='EXPORT INTERRUPTED; RETRY'
return
print(','.join(csv_cell(v) for v in row))
if rows:
last=list(rows[-1]);last[2]='META';last[14]='dropped_samples:'+str(logs.dropped_samples)+' dropped_events:'+str(logs.dropped_events)
print(','.join(csv_cell(v) for v in last))
logs.exported=True
message='CSV EXPORTED TO CONSOLE'
def edit_config(key):
global pending,message
if active or competition.is_enabled():return
version,kp,cap=pending
if key=='KP-':kp=max(.02,round(kp-.01,2))
elif key=='KP+':kp=min(.20,round(kp+.01,2))
elif key=='CAP-':cap=max(5,cap-1)
elif key=='CAP+':cap=min(20,cap+1)
else:return
if (kp,cap)!=(pending[1],pending[2]):
pending=(version+1,kp,cap)
message='PENDING CONFIG; NEXT RUN ONLY'
def cancelled():
return (not competition.is_enabled() or not competition.is_autonomous()
or controller.buttonB.pressing())
def run_demo(run,config):
global message
started=brain.timer.time(MSEC)
previous_ms=started
next_sample=started
measured=0.0
previous_request=0.0
state='APPROACH'
settle_since=None
result='EXCEPTION'
gap=cycle=0
publish(make_record(run,config,'EVENT',0,state,measured,0,0,0,'START'),True)
try:
while True:
before=brain.timer.time(MSEC)
gap=before-previous_ms
if cancelled():result='CANCELLED';break
if gap>MAX_GAP_MS:result='UPDATE_GAP';break
if before-started>=RUN_LIMIT_MS:result='DEADLINE';break
# Toy plant: 1 percent request -> 10 mm/s, no inertia/slip/load.
# This is NOT a model suitable for predicting an actual VEX robot.
measured+=previous_request*10*gap/1000.0
error=TARGET_MM-measured
request=max(0.0,min(config[2],config[1]*error))
if abs(error)<=5:
request=0.0
if settle_since is None:
settle_since=before
state='SETTLING'
publish(make_record(run,config,'EVENT',before-started,state,measured,0,gap,0,'ENTER_SETTLING'),True)
if before-settle_since>=200:result='DONE';break
after=brain.timer.time(MSEC)
cycle=after-before # Computational duration, excluding deliberate yield.
if cancelled():result='CANCELLED';break
if after-started>=RUN_LIMIT_MS:result='DEADLINE';break
if after-previous_ms>MAX_GAP_MS:result='UPDATE_GAP';break
record=make_record(run,config,'SAMPLE',after-started,state,measured,request,gap,cycle)
if after>=next_sample:
publish(record)
next_sample=after+LOG_MS # No catch-up burst after missed slots.
previous_request=request
previous_ms=after
wait(POLL_MS,MSEC)
except Exception:
result='EXCEPTION'
finally:
previous_request=0.0 # No physical output exists in this demo.
publish(make_record(run,config,'RESULT',brain.timer.time(MSEC)-started,
result,measured,0,gap,cycle,result),True)
message='FINAL '+result+'; EXPORT WHILE DISABLED'
return result
def autonomous():
global active,run_number,message
if active:return
if not logs.exported:
message='EXPORT OR CLEAR PREVIOUS LOG'
return
config=pending # Frozen tuple; this run never reads edited pending gains.
if (not math.isfinite(config[1]) or not .02<=config[1]<=.20
or not math.isfinite(config[2]) or not 5<=config[2]<=20):
message='INVALID CONFIG';return
active=True
run_number+=1
logs.__init__() # Start a fresh buffer only after export or deliberate clear.
try:
run_demo(run_number,config)
finally:
active=False
def driver_control():
# run_demo checks the competition mode; this project has no driver output.
pass
BUTTONS=[('KP-',10,150,100,36),('KP+',120,150,100,36),
('CAP-',240,150,100,36),('CAP+',350,150,100,36),
('EXPORT',10,195,220,35),('CLEAR',250,195,220,35)]
def hit(x,y):
for key,bx,by,w,h in BUTTONS:
if bx<=x<bx+w and by<=y<by+h:return key
return None
def clear_log():
global published,message
if active or competition.is_enabled():return
logs.__init__()
published=None
message='LOG CLEARED; NO MOTOR OUTPUT'
def display_number(value):
return 'NA' if value is None else str(round(value,1))
def draw_dashboard():
screen=brain.screen
screen.clear_screen(Color.BLACK)
screen.set_font(FontType.MONO15)
screen.set_pen_color(Color.WHITE)
screen.print_at(message,x=10,y=20,opaque=False)
screen.print_at('Pending cfg '+str(pending[0])+' KP '+str(pending[1])+' cap '+str(pending[2]),x=10,y=42,opaque=False)
snapshot=published
if snapshot is None:
lines=['NO RUN DATA','Enable autonomous for software demo','Controls work only while disabled']
else:
stamp,r=snapshot
age=max(0,brain.timer.time(MSEC)-stamp)
label='LIVE' if active else 'FINAL'
lines=[label+' run '+str(r[1])+' '+r[4]+' age '+str(int(age))+' ms',
'Target '+str(r[5])+' measured '+display_number(r[6])+' error '+display_number(r[7]),
'Run KP '+str(r[9])+' cap '+str(r[10])+' request '+display_number(r[8])]
for y,text in zip((68,91,114),lines):screen.print_at(text,x=10,y=y,opaque=False)
for key,x,y,w,h in BUTTONS:
screen.set_fill_color(Color.BLACK)
screen.set_pen_color(Color.WHITE)
screen.draw_rectangle(x,y,w,h)
screen.print_at(key,x=x+10,y=y+24,opaque=False)
competition=Competition(driver_control,autonomous)
was_pressed=False
pressed_key=None
pressed_at=0
next_screen=0
while True:
now=brain.timer.time(MSEC)
pressed=brain.screen.pressing()
if active or competition.is_enabled():
pressed_key=None
elif pressed and not was_pressed:
pressed_key=hit(brain.screen.x_position(),brain.screen.y_position())
pressed_at=now
elif not pressed and was_pressed and 80<=now-pressed_at<=2000:
key=pressed_key
pressed_key=None
if key=='EXPORT':export_csv()
elif key=='CLEAR':
clear_log()
elif key is not None:edit_config(key)
was_pressed=pressed
if now>=next_screen:
draw_dashboard() # Only this loop draws the screen.
next_screen=brain.timer.time(MSEC)+SCREEN_MS
wait(POLL_MS,MSEC)
After the run, disable and press EXPORT to print the CSV. Export or deliberately CLEAR the previous log before another demonstration. The buffers are volatile and are lost if the project restarts or the Brain loses power.
The dashboard is drawn by the main loop; the callback publishes records. This is a teaching handover pattern, not a general thread-safe logging framework. Keep the ownership and interruption checks from 09.4 when connecting real movement.
08 | UNDERSTAND THE CSV SCHEMA
| Column | Meaning |
|---|---|
| code_id / run_id | Code/config identity and run number within this project session |
| kind / t_ms / state | SAMPLE, EVENT, RESULT or META; elapsed milliseconds; action state |
| target_mm / measured_mm / error_mm | Target and model observation in mm; error = target - measured |
| request_pct | Requested output percentage, not actual motor speed |
| kp_pct_per_mm / cap_pct | Frozen gain and cap used by this run |
| gap_ms / cycle_ms | Interval since prior completed update; measured computation duration |
| valid / reason | Validity flag and explicit event or outcome reason |
The META row carries buffer drop counts. A RESULT row remains separate from the periodic trace. Keep failed, cancelled and deadline-expired runs in the dataset.
Use a unique session or trial identifier when combining logs across project restarts, because the in-memory run counter starts again. Add route version, battery identity and physical outcome to the trial record.
09 | MOVE FROM THE MODEL TO A REAL ROUTE
Replace the toy plant update with your existing feedback and guarded action update. Read actual travel or pose, derive error using the same units and record the requests that your output owner commits.
Do not paste another Competition object, movement loop or conflicting device declaration into an existing project. Reuse its control owner, state machine, estimator and cancellation policy.
# Integration outline inside your existing control tick: # 1. Acquire and validate feedback. # 2. Check cancellation and action/route deadlines. # 3. Calculate and validate the requested output. # 4. Commit through the single output owner. # 5. Capture a small record when due, plus any state event. # 6. Publish a display snapshot without printing/file I/O here.
Measure logging overhead. The example cycle_ms covers control computation before recording; it does not include every buffer operation, display call or deliberate yield. The next gap check can reveal later delays, but it is not an independent hardware watchdog.
Stop or revoke motion before a logging export. A logger failure policy must be explicit: do not accidentally hide a control failure or let a secondary telemetry exception bypass cleanup.
10 | SAVE LOGS AFTER THE RUN
Console CSV export is the simplest starting point. Copy the header and data rows into a spreadsheet or CSV file, preserving units and validity flags.
The VEX SD-card API provides savefile and appendfile using bytearrays. If you add file export, use a unique filename or deliberately handle an existing one; savefile overwrites an existing file.
Check card availability, report write failures and verify the saved file on a computer. Write in bounded batches while disabled, outside the motion loop. The complete starter uses console export and does not write an SD card.
11 | READ THE TRACE BEFORE CHANGING GAINS
| Pattern | Possible explanation | Next comparison |
|---|---|---|
| Error stays large; request remains capped | Saturation, excessive load or unreachable target | Measured motion, battery/temperature and cap policy |
| Error changes sign repeatedly | Overshoot, aggressive response, noise or timing issues | Plot feedback and requests around the target |
| Request becomes zero but robot still moves | Physical inertia or inappropriate stopping behaviour | Independent motion and braking measurements |
| Loop-gap spikes align with screen/log operations | Measurement work may disturb timing | Compare telemetry enabled and reduced |
| Controller reports DONE; measured endpoint misses | Estimator error, slip or success-rule mismatch | External position measurement |
Compare repeated trials using the same start, load, conditions and success criteria. Look at worst observed error and failure rate as well as a typical successful trace.
12 | COMMON MISTAKES
- Printing at every control tick: buffer and export later.
- Editing live gains mid-run: freeze the active configuration.
- Showing a stale value without its age: label freshness and final state.
- Mixing requests with measurements: use explicit column names.
- Only recording successful runs: retain faults and cancellations.
- Allowing unbounded lists: cap memory and count dropped records.
- Overwriting an unsaved trial: export or clear deliberately.
- Transferring toy-model gains to hardware: calibrate and test the real controller.
CHALLENGES | DISPLAY, TUNE AND RECORD
- Trace: identify the START, settling and RESULT events in the CSV.
- Freeze: prove that a run uses its captured settings rather than pending values.
- Compare: change one demo gain and compare duration and request saturation.
- Limit: reduce sample capacity and verify the exported drop count.
- Interrupt: cancel a run and inspect its terminal record.
- Measure: compare loop gaps with two screen-refresh settings.
- Integrate: add a small telemetry record to a guarded physical practice route.
- Analyse: compare its internal DONE result with externally measured endpoint error.
Success evidence
Consistent CSV headers, frozen settings, state events, cancellation results, buffer-limit evidence and measured overhead. Physical trials also need independent outcome measurements.
OFFICIAL REFERENCES
- Brain-screen display and touch API
- Competition callbacks and mode API
- SD-card file saving and availability API
The buffer, configuration policy and software model are independently written teaching examples. Validate timing and physical behaviour when integrating telemetry with a real robot.