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

Choose a small useful set before adding more fields
CategoryUseful fieldsReason
IdentityRun ID, code/config version, route IDCompare like with like
Target and feedbackTarget distance, measured travel, heading or poseSee the error the controller sees
ControlError, requested output, output capExplain correction and saturation
State and outcomeAction state, transition, cancellation or fault reasonLocate when the route changed
TimingElapsed time, loop gap, computation timeIdentify delayed updates
QualityValidity, age, gate resultAvoid mistaking stale or rejected evidence for current data
ConditionsBattery, temperatures, surface and loadExplain 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

These are example schedules, not hard real-time guarantees
WorkIllustrative ratePolicy
Control updatesEvery 20 msCompute from current feedback and keep cancellation responsive
Sample recordsEvery 100 msStore a small bounded record
Brain displayEvery 200 msDraw the latest published snapshot
Console or file exportAfter the run, while disabledPerform 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

The demo exports one consistent fifteen-column schema
ColumnMeaning
code_id / run_idCode/config identity and run number within this project session
kind / t_ms / stateSAMPLE, EVENT, RESULT or META; elapsed milliseconds; action state
target_mm / measured_mm / error_mmTarget and model observation in mm; error = target - measured
request_pctRequested output percentage, not actual motor speed
kp_pct_per_mm / cap_pctFrozen gain and cap used by this run
gap_ms / cycle_msInterval since prior completed update; measured computation duration
valid / reasonValidity 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

Each pattern suggests a hypothesis, not a confirmed cause
PatternPossible explanationNext comparison
Error stays large; request remains cappedSaturation, excessive load or unreachable targetMeasured motion, battery/temperature and cap policy
Error changes sign repeatedlyOvershoot, aggressive response, noise or timing issuesPlot feedback and requests around the target
Request becomes zero but robot still movesPhysical inertia or inappropriate stopping behaviourIndependent motion and braking measurements
Loop-gap spikes align with screen/log operationsMeasurement work may disturb timingCompare telemetry enabled and reduced
Controller reports DONE; measured endpoint missesEstimator error, slip or success-rule mismatchExternal 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

  1. Trace: identify the START, settling and RESULT events in the CSV.
  2. Freeze: prove that a run uses its captured settings rather than pending values.
  3. Compare: change one demo gain and compare duration and request saturation.
  4. Limit: reduce sample capacity and verify the exported drop count.
  5. Interrupt: cancel a run and inspect its terminal record.
  6. Measure: compare loop gaps with two screen-refresh settings.
  7. Integrate: add a small telemetry record to a guarded physical practice route.
  8. 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

The buffer, configuration policy and software model are independently written teaching examples. Validate timing and physical behaviour when integrating telemetry with a real robot.