|
LightLib
PROS library for VEX V5: EKF/MCL localization, RAMSETE path following, high-level chassis API
|
This tutorial covers smooth trajectory following — driving along a curved path with continuous velocity instead of stop-and-pivot motion. LightLib implements RAMSETE (a nonlinear pose tracker) on top of a quintic Hermite spline + trapezoidal velocity profile.
pid_odom_set with a vector of waypoints chains boomerang motions, but each segment is a separate "go to that point" with its own acceleration profile. The robot momentarily slows at every waypoint. For a smooth serpentine across the field, you want one continuous trajectory that the robot tracks at a controlled velocity throughout.
A trajectory is a function (x(t), y(t), θ(t), v(t), ω(t)) defined every 10 ms — the robot's desired pose and velocity at every moment. RAMSETE ("Rapidly converging Asymptotically stable Method for SE(2) Tracking") is a controller that takes:
(x_d, y_d, θ_d) from the trajectoryv_d and angular velocity ω_d(x, y, θ) from odometry…and produces an actual (v, ω) command that drives the robot back onto the trajectory. The controller has two parameters:
b** (aggressiveness) — how hard it corrects pose error. Larger → faster correction but jerkier. Typical: 1.0–2.5.zeta** (damping) — how much it damps velocity error. 0 = pure proportional, 1 = critically damped. Typical: 0.7.Underneath, each tank-side wheel velocity is mapped from (v, ω):
Then a per-wheel feedforward + P controller turns those into voltages:
kS, kV, kA are the motor constants you identify with characterize_kV_kA_kS (next tutorial). kP is small — a few centivolts per (in/s) error. RAMSETE handles the geometry; the feedforward handles the motor dynamics.
LightLib generates trajectories from waypoints in two passes:
Velocity pass — walk the spline and assign a velocity at every 10 ms tick, respecting four constraints:
vMax — max linear speed (in/s)aMax — max forward accel (in/s²)aDecMax — max deceleration (in/s²)aLatMax — max centripetal accel (v² / R ≤ aLatMax → forces a slowdown in tight curves)The velocity pass is a forward-backward sweep: forward to enforce accel limits from the start, backward to enforce decel limits before each waypoint and the end, with the lateral cap clamped throughout. Result: the fastest profile that respects every constraint.
That's the entire pipeline. The result is a Trajectory object — a dense table of TrajState{x, y, θ, v, ω, a, κ, t} sampled every 10 ms.
Path following needs three structs configured once in initialize(), before any auton runs:
The numbers above are reasonable starting points for a blue-cart 11.5"
tank chassis on 3.25" wheels. The kS / kV / kA values come from running characterize_kV_kA_kS (next tutorial). The kinematic limits start conservative — once the feedforward is right, you can push vMax up toward 60 in/s.
followTrajectory blocks until the robot reaches the end (or bails on timeout / pose error). The return value tells you whether it completed cleanly.
If you omit headingRad on an interior waypoint, the spline picks a heading that's a smooth blend of the chords on either side. Set headingRad only when you specifically need a particular tangent direction (start of path, end of path, sharp turn at a known point).
The robot drives the entire path backwards. Useful for backing into a goal — define the path as if going forward, then flip the bit.
poseErrBailIn is the safety net: if the robot's pose error exceeds this many inches at any point during the trajectory (e.g. someone is shoving it, or odom desynced), the follower gives up and returns false. Tune based on how aggressive your paths are.
Fire an action at a specific waypoint mid-path:
atWaypoint is the zero-based index in your wps vector. The action fires on the 10 ms control tick where the trajectory's time first crosses the time at which the trajectory passes that waypoint. Each event fires at most once.
Callbacks may call light::setPose() to re-zero odometry mid-path — the next tick will read the new pose and RAMSETE's error term will correct against the unchanged trajectory. This is a clean way to handle "I rammed
the wall, my IMU drifted, now I'm fixing it" mid-auton.
path.jerryio.com is a browser-based path designer. You drag waypoints around a field and it exports a CSV. LightLib parses that CSV directly:
The CSV format: each non-empty, non-#-commented line has 2 or 3 floats (comma, tab, or whitespace separated). x, y or x, y, heading_rad. Units are inches and radians (LightLib convention: +Y forward, theta=0 faces +Y, CW positive). Extra columns past 3 are ignored, so exports with speed/lookahead fields still work.
For paths too big to compile in:
Same parser, just reads the file at runtime. Useful if you want to update paths without re-flashing.
For multi-auton programs, register paths by name:
It's just a thin lookup over kAll[]. Lookup is O(N) string compare — fine for the small number of paths a single robot carries. Unknown names print a warning and return false without invoking the follower.
If the robot tracks the path with persistent error in one direction:
kV is too low, kS is too low, or the chassis has uneven friction. Re-run characterize_kV_kA_kS and characterize_friction.kA is too low, or vMax is optimistic. Drop vMax first, then re-tune kA.b is too high. Drop from 2.0 to 1.5.b is too low. Bump up.kP is too high. The default of 0.02 is conservative; if you've raised it, drop back down.Start with the defaults, get characterized values, then tune b last.
Robot tracks well at low vMax but oscillates at high vMax. Almost always means kA is wrong — probably too low. Re-run characterize_kV_kA_kS with a longer test distance (so the accel phase gives the fitter more samples).
First waypoint heading is "wrong" on launch. You set headingRad on the first waypoint to a value that doesn't match the actual robot start heading. Either match it, or call light::setPose() to match the robot to the path before running.
Path completes but ends rotated 5° off. End-of-path heading isn't RAMSETE's responsibility — it tracks the trajectory until the trajectory ends, and the final tangent of the spline determines the heading. If you need a precise final heading, follow the path with runJerryioPath and then queue a pid_turn_set to lock the heading: