Finite State Machines (FSMs)¶
This project uses Finite State Machines (FSMs) implemented as Python generators operating within a cooperative scheduler. Each task’s run() method yields the current state, allowing the scheduler to manage execution context.
Summary & Interaction¶
task_user: Command hub; enables/disables other tasks
task_motor: Two instances (left/right); controlled by goFlag from task_user or task_competition
task_observer: Provides center distance used by task_competition for segment tracking
task_reflectance: Provides centroid & line_found used by task_line_follow and task_competition
task_line_follow: Steers motors based on centroid; gains & setpoint set by task_user or task_competition
task_imu: Provides heading used for rotation tracking; calibration managed via task_user
task_ultrasonic: Provides distance; monitored by task_competition during S5 (garage approach)
task_competition: Orchestrates entire race; controls setpoints, enables/disables subsystems per segment
1. User Interface Task (task_user.py)¶
Purpose: Accept user commands, configure system parameters, and trigger subsystem operations.
State Diagram¶
S0_PROMPT ──→ S1_CMD ──→ [Decision Point]
├─→ S2_HELP ──→ S0_PROMPT
├─→ S3_GAINS ──→ S0_PROMPT
├─→ S4_SETPOINT ──→ S0_PROMPT
├─→ S5_COLLECT ──→ S0_PROMPT
├─→ S6_DEBUG ──→ S0_PROMPT
├─→ S7_CALIBRATION ──→ S0_PROMPT
├─→ S8_LINEFOLLOW ──→ S0_PROMPT
├─→ S9_IMU_MENU ──→ S0_PROMPT
└─→ S10_STATE_ESTIMATION ──→ S0_PROMPT
States¶
State |
Code |
Purpose |
Behavior |
|---|---|---|---|
S0_PROMPT |
0 |
Display menu |
Print help menu once, transition to S1_CMD |
S1_CMD |
1 |
Await input |
Poll serial for single character command |
S2_HELP |
2 |
Show help |
Display help text, wait for Enter |
S3_GAINS |
3 |
Configure PID |
Import and delegate to |
S4_SETPOINT |
4 |
Set velocity |
Import and delegate to |
S5_COLLECT |
5 |
Data logging |
Collect motor velocity data while running test |
S6_DEBUG |
6 |
Debug menu |
Import and delegate to |
S7_CALIBRATION |
7 |
Calibrate sensor |
Import and delegate to |
S8_LINEFOLLOW |
8 |
Line follow setup |
Import and delegate to |
S9_IMU_MENU |
9 |
IMU control |
Import and delegate to |
S10_STATE_ESTIMATION |
10 |
State observer |
Enable observer and line follower for debugging |
Command Mapping (S1_CMD)¶
Command |
Action |
Destination |
|---|---|---|
|
Show help menu |
S2_HELP |
|
Enter PID gains |
S3_GAINS |
|
Set setpoint |
S4_SETPOINT |
|
Start motor test |
S5_COLLECT |
|
Calibrate reflectance |
S7_CALIBRATION |
|
Debug menu |
S6_DEBUG |
|
Line follow config |
S8_LINEFOLLOW |
|
State estimation |
S10_STATE_ESTIMATION |
|
IMU menu |
S9_IMU_MENU |
|
Toggle memory monitor |
(toggle flag, stay in S1_CMD) |
Enter |
Return to menu |
S0_PROMPT |
Key Behaviors¶
S3–S9: Import UI modules, delegate execution via
yield from, then unload module to save memoryS5_COLLECT: Enables motor test, waits for queue saturation, then prints CSV data
S10_STATE_ESTIMATION: Runs observer + line follower continuously, prints debug values each cycle
2. Motor Control Task (task_motor.py)¶
Purpose: Implement closed-loop PI velocity control for each motor with encoder feedback.
State Diagram¶
S0_INIT → S1_WAIT ↔ S2_RUN
States¶
State |
Code |
Purpose |
Behavior |
|---|---|---|---|
S0_INIT |
0 |
Initialization |
Placeholder (can be removed); immediately transitions to S1_WAIT |
S1_WAIT |
1 |
Idle |
Monitor |
S2_RUN |
2 |
Active control |
Execute PI controller loop, update encoder state, collect profiling data if enabled |
State Transitions¶
S1_WAIT → S2_RUN
Trigger:
goFlag.get()becomes non-zeroInitialization:
Capture start timestamp (
startTime)Reset PI controller
Zero encoder position
Load gain values (Kp, Ki) and setpoint from shares
Enable motor driver (set voltage)
S2_RUN → S1_WAIT
Trigger:
goFlag.get()becomes zeroCleanup:
Disable motor (zero voltage)
Reset encoder
S2_RUN Cycle Actions¶
Each iteration:
Update Parameters — Reload setpoint from
setpointshare (allows real-time tuning)Capture Time — Get current timestamp
Encoder Update — Read encoder, calculate velocity
PI Control — Call controller; updates motor voltage via callback
Status Update — Publish encoder position to
wheelDistanceshareVelocity Publishing — Compute angular velocity (ω = v / WHEEL_RADIUS), publish to
motorOmegashareProfiling (if goFlag==2):
Extract velocity from encoder
Enqueue velocity →
dataValuesqueueEnqueue time (ms since start) →
timeValuesqueue
Testing¶
Motor Test Mode:
User triggers via command
g(S5_COLLECT in task_user)Sets
rightMotorGo=2(profiling mode),leftMotorGo=1(normal mode)Data queues capture velocity over time for plot/analysis
3. Observer/State Estimator Task (task_observer.py)¶
Purpose: Estimate robot center position from dual encoder measurements.
Current Status: Simplified (No Active FSM)¶
The FSM structure exists (S0_IDLE, S1_RUN) but is commented out. Current behavior is deterministic:
States (Defined but Unused)¶
State |
Code |
Purpose |
|---|---|---|
S0_IDLE |
0 |
Idle state (unused) |
S1_RUN |
1 |
Running state (unused) |
Actual Behavior¶
Each cycle (no state branching):
Calculate center distance:
centerDistance = 0.5 × (leftWheelDist + rightWheelDist)Publish — Set
observerCenterDistanceshareYield — Return control to scheduler
Future Enhancement¶
Full observer implementation would include:
Kalman filtering or discrete integration
Heading-based position estimates
Gyro/IMU integration for rotation tracking
4. Reflectance Sensor Task (task_reflectance.py)¶
Purpose: Manage line sensor calibration and continuous centroid tracking.
State Diagram¶
S0_IDLE ←→ S1_CALIB_DARK
↓
←→ S2_CALIB_LIGHT
↓
←→ S3_RUN ←─┐
└─ (loop while running)
States¶
State |
Code |
Purpose |
Trigger |
|---|---|---|---|
S0_IDLE |
0 |
Idle |
Default state |
S1_CALIB_DARK |
1 |
Calibrate dark |
|
S2_CALIB_LIGHT |
2 |
Calibrate light |
|
S3_RUN |
3 |
Read centroid |
|
State Transitions¶
S0_IDLE Behavior
Poll
reflectanceModeshare:reflectanceMode = 0→ stay in S0_IDLEreflectanceMode = 1→ S1_CALIB_DARKreflectanceMode = 2→ S2_CALIB_LIGHTreflectanceMode = 3→ S3_RUN (save start time)Other → raise ValueError
S1_CALIB_DARK & S2_CALIB_LIGHT
Entry: Call
sensor.calibrate("dark")orsensor.calibrate("light")→ returns generatorLoop: Iterate through calibration generator until completion
Exit: When calibration generator exits (StopIteration)
Post-Exit: Set
reflectanceMode = 0, return to S0_IDLE
S3_RUN Loop
Entry: Save
runStartTimefor CSV loggingEach Cycle:
Read sensor:
raw, calibrated, centroid, line_found = sensor.get_values()Publish centroid →
lineCentroidsharePublish detection →
lineFoundshare (boolean)If queues not full:
Enqueue centroid →
centroidValuesEnqueue elapsed time (ms) →
centroidTimeValues
Exit Condition:
reflectanceModebecomes 0Post-Exit: Return to S0_IDLE
Data Format¶
Centroid: float −1.0 to +1.0 (−1 = far left, 0 = center, +1 = far right)
Line Found: boolean (True if contrast > threshold)
5. Line Following Task (task_line_follow.py)¶
Purpose: PI controller that steers robot to keep line at sensor center.
State Diagram¶
State 0 (Idle) ↔ State 1 (Running)
States¶
State |
Code |
Purpose |
Behavior |
|---|---|---|---|
Idle |
0 |
Waiting |
Monitor |
Running |
1 |
Active control |
Execute PI controller each cycle using centroid as input |
State Transitions¶
Idle → Running
Trigger:
lineFollowGo.get()becomes non-zeroInitialization:
Load Kp, Ki from
lineFollowKp,lineFollowKisharesLoad nominal setpoint (straight-line speed in mm/s)
Calculate feed-forward angular velocity (assumes 300mm radius circle):
omega_ff = nominalSetPoint / 300.0
Apply feed-forward gain:
controller.set_feed_forward(omega_ff, Kff)Kff initially 0; set externally during course following
Reset PI error integrator
Running → Idle
Trigger:
lineFollowGo.get()becomes zeroCleanup: Disable controller
Running Cycle Actions¶
Update Setpoint — Reload straight-line speed from share (allows real-time tuning)
Run Controller — Call
controller.run()Automatically reads
lineCentroidfrom shareComputes output (steering correction in mm/s)
Calls callback with output value
Apply Output — Callback
_plant_cb(output)sets motor speeds:left_speed = nominalSetPoint + output right_speed = nominalSetPoint - output
Positive output (line on right) → slow right, speed up left (turn left)
Negative output (line on left) → slow left, speed up right (turn right)
Feed-Forward Tuning¶
Initial value: 0 (no FF)
Leg 2 (tight curve): 0.6 (aggressive turn assistance)
Later segments: 0.3 (gentle correction)
Set externally via
lineFollowKffshare during competition course execution
6. IMU Task (task_imu.py)¶
Purpose: Initialize, configure, calibrate, and continuously read BNO055 9-DOF IMU sensor.
State Diagram¶
S0_BEGIN
↓
S1_CALIBRATE (optional)
↓
S2_IDLE ←─────────────────┐
├→ S3_RUN_NDOF ────────┤
├→ S4_SAVE_CALIB ──────┤
├→ S5_LOAD_CALIB ──────┤
├→ S6_TARE ────────────┤
├→ S7_READ_VALS ───────┤
└→ S8_GET_CALIB_STATE ─┘
States¶
State |
Code |
Purpose |
Mode Value |
|---|---|---|---|
S0_BEGIN |
0 |
Startup |
Initialization only |
S1_CALIBRATE |
1 |
Auto-calibrate |
Wait for sys/gyro/accel/mag all = 0x03 |
S2_IDLE |
2 |
Command wait |
Monitor mode share; dispatch to other states |
S3_RUN_NDOF |
3 |
Stream data |
Continuous heading & rate output (mode=0xFF) |
S4_SAVE_CALIB |
4 |
Save offsets |
Write IMU calibration to file (mode=2) |
S5_LOAD_CALIB |
5 |
Load offsets |
Read and apply calibration (mode=1) |
S6_TARE |
6 |
Tare accel/gyro |
Capture zero-point references (mode=3) |
S7_READ_VALS |
7 |
One-shot read |
Read heading & rate once (mode=4) |
S8_GET_CALIB_STATE |
8 |
Calib status |
Query and pack calibration flags (mode=5) |
State Transitions¶
S0_BEGIN
Actions:
Call
imu.initialize(NDOF_OP_MODE)— cooperative initializationAttempt to load saved calibration via
_load_calibration()
Conditional Exit:
If load succeeds → skip S1_CALIBRATE, go to S2_IDLE
If load fails → (currently still goes to S2_IDLE; S1_CALIBRATE optional)
S1_CALIBRATE (Entry → S2_IDLE when calibrated)
Poll calibration status each cycle
If all sensors (sys, gyro, accel, mag) reach 0x03 (fully calibrated):
Save calibration via
_save_calibration()Transition to S2_IDLE
S2_IDLE (Hub State)
Poll
imuModeshare:imuMode = 0→ stay in S2_IDLEimuMode = 1→ S5_LOAD_CALIBimuMode = 2→ S4_SAVE_CALIBimuMode = 3→ S6_TAREimuMode = 4→ S7_READ_VALSimuMode = 5→ S8_GET_CALIB_STATEimuMode = 0xFF→ S3_RUN_NDOFOther → set flag, stay in S2_IDLE
All Command States (S3–S8) → S2_IDLE
Exit: Set
imuMode = 0(acknowledge command), return to S2_IDLE
State Actions¶
S3_RUN_NDOF — Continuous Heading/Rate Output
First Entry Only: Tare heading (capture zero reference)
Each Cycle:
Read gyro:
gx, gy, gz = imu.gyro()Read Euler angles:
h, r, p = imu.euler()Update
headingshare with hUpdate
headingRateshare with gz (yaw rate)Yield and return to S2_IDLE (recheck mode each cycle)
S4_SAVE_CALIB — Export Calibration
Call
_save_calibration():Get calibration offsets from IMU (22 bytes)
Get tare values (accel_xyz, gyro_xyz)
Write JSON to
IMU_FILE
Return to S2_IDLE
S5_LOAD_CALIB — Import Calibration
Call
_load_calibration():Read JSON from
IMU_FILEExtract offsets (22 bytes), apply via
imu.set_calibration_offsets()Extract & apply tare values (accel_xyz, gyro_xyz)
Return to S2_IDLE
S6_TARE — Capture Zero Reference
Call
imu.tare_accel_gyro()(default: 100 samples, 5 ms delay)Averages acceleration and gyro readings while stationary
Stores as offset in accel_tare, gyro_tare
Also saves to file via
_save_calibration()
Return to S2_IDLE
S7_READ_VALS — One-Shot Reading
Read gyro, Euler angles
Update
heading,headingRatesharesReturn to S2_IDLE
S8_GET_CALIB_STATE — Query Calibration Flags
Get
sys, gyro, accel, mag = imu.calibration_status()Pack into one byte:
result = (sys << 6) | (gyro << 4) | (accel << 2) | mag
Update
imuCalibrationshare with resultReturn to S2_IDLE
Calibration Status Values¶
0 – Not calibrated
1 – Barely calibrated
2 – Good calibration
3 – Fully calibrated
7. Ultrasonic Distance Task (task_ultrasonic.py)¶
Purpose: Continuous distance measurement from ultrasonic sensor.
Structure: No FSM¶
This task is a simple loop with no state machine:
Loop:
1. Call sensor.loop()
2. Read distance: d = sensor.get_distance()
3. If d is None:
ultrasonicDistance.put(0)
Else:
ultrasonicDistance.put(d)
4. Yield
Behavior¶
Runs indefinitely — yields each cycle, no state transitions
Updates share:
ultrasonicDistancewith measured distance in mmInvalid readings: Publishes 0 on sensor error/timeout
8. Competition Course Task (task_competition.py)¶
Purpose: Navigate a multi-leg autonomous course with line following, turns, curves, and obstacles.
Course Layout¶
Leg |
Segment |
Description |
Distance |
|---|---|---|---|
1 |
S1 |
Accelerate/decelerate straight |
1375 mm |
2 |
S2–S3 |
Curved segment (R200 → R125) |
~50+ mm |
2 |
S4–S5 |
Garage approach (slow on wall approach) |
~205 mm |
2 |
S6–S7 |
Exit garage, resume line follow |
Variable |
3 |
S8–S9 |
Long segment with U-turn |
~1800 mm |
3 |
S10–S11 |
U-turn and re-acquisition |
Variable |
4 |
S12–S14 |
Final segments to finish |
~150 mm |
State Diagram¶
S0 → S1 → S2 → S3 → S4 → S5 → S6 → S7 → S8 → S9 → S10 → S11 → S12 → S13 → S14 → S0
States & Actions¶
State |
Code |
Entry Trigger |
Exit Condition |
Next State |
|---|---|---|---|---|
S0 |
0 |
Reset/abort |
|
S1 |
S1 |
1 |
Race start |
Distance ≥ 1375 mm |
S2 |
S2 |
2 |
After accel |
Line lost |
S3 |
S3 |
3 |
Curve started |
Distance ≥ 50 mm |
S4 |
S4 |
4 |
Garage entry |
Distance ≥ 205 mm |
S5 |
S5 |
5 |
In garage |
Ultrasonic < 5 mm |
S6 |
S6 |
6 |
At wall |
Line + centroid |
S7 |
S7 |
7 |
Exit garage |
Distance + no line |
S8 |
S8 |
8 |
Sharp turn |
Line + centroid |
S9 |
S9 |
9 |
Resume follow |
Distance ≥ 1800 mm |
S10 |
S10 |
10 |
Start U-turn |
Arc dist ≥ 167 mm |
S11 |
S11 |
11 |
Complete turn |
Line found |
S12 |
S12 |
12 |
Re-acquire |
Distance + no line |
S13 |
S13 |
13 |
Final approach |
Distance ≥ 50 mm |
S14 |
S14 |
14 |
Final segment |
Distance ≥ 100 mm |
S0 |
S0 (Idle/Abort)¶
Entry Condition: Initial state or abort detected
Exit Condition: competitionGoFlag becomes 1
Initialization (on exit):
Start timer: save
ticks_ms()for time-based acceleration rampsSave center distance reference:
_centerStartDist = observerCenterDistance.get()Enable all subsystems:
reflectanceMode = 3(continuous line reading)observerGoFlag = 1(enable state estimator)leftMotorGo = rightMotorGo = 1(motors active)lineFollowGo = 1(line follower active)
Initialize motion:
Line follower setpoint = 100 mm/s
Feed-forward gain = 0
S1 (Accelerate/Decelerate Leg)¶
Entry: From S0
Distance Tracking:
segmentDistance = observerCenterDistance.get() - _centerStartDist
Acceleration Phase (0–550 mm):
If velocity < 500 mm/s:
velocity += ACCELERATION × dt(ramp up)ACCELERATION = 500 mm/s²
Deceleration Phase (1000–1375 mm):
If velocity > 100 mm/s:
velocity -= ACCELERATION × dt(ramp down)
Each Cycle:
Calculate elapsed time since last cycle
Apply accel/decel based on distance
Update line follower setpoint:
lineFollowSetPoint.put(velocity)
Exit Condition: segmentDistance ≥ 1375 mm
On Exit:
Set final velocity = 100 mm/s
Enable feed-forward:
lineFollowKff = 0.6(Leg 2 tight curve)Reset distance reference
S2 (Follow Short Radius Curve, R200)¶
Entry: From S1
Behavior:
Line follower disabled; motors run at fixed R200 radius:
wheel_speeds(200, 100) → left_speed, right_speed
Exit Condition: Line is lost (lineFound = False)
On Exit:
Disable line follower
Set motors for tighter R125 curve
Reset distance reference
S3 (Move at Radius Speed, R125)¶
Entry: From S2
Behavior: Maintain fixed R125 radius motor speeds
Exit Condition: segmentDistance ≥ 50 mm
On Exit:
Set motors equal forward:
left = right = 100mm/sClear feed-forward
Reset distance reference
S4 (Garage Forward)¶
Entry: From S3
Behavior: Move robot straight forward into garage at 100 mm/s
Exit Condition: segmentDistance ≥ 205 mm
On Exit:
Reset motors to equal moderate speed
Transition to S5
S5 (Approach Wall, Ultrasonic Feedback)¶
Entry: From S4
Ultrasonic Control:
If distance 20–100 mm:
setpoint = 2.5 × distance + 50
Slow approach as distance decreases
If distance < 5 mm:
Start rotation to find exit line
Set motors:
left = -75,right = +75(CCW rotation)Disable line follower
Return to S0 (abort) or transition to S6
S6 (Rotate to Find Line)¶
Entry: From S5, CCW rotation active
Exit Condition: Line detected with centroid ≤ −0.4 (line on left edge)
On Exit:
Enable line follower: setpoint = 100 mm/s
Clear feed-forward
Reset distance reference
S7 (Line Follow Past Intersection)¶
Entry: From S6
Behavior: Continue line following past parking garage exit
Exit Condition: segmentDistance ≥ 250 mm AND line lost (lineFound = False)
On Exit:
Disable line follower
Set motors for sharp right turn:
wheel_speeds(30, 25)(R30 radius)Reset distance reference
S8 (Sharp Right Turn)¶
Entry: From S7
Behavior: Maintain R30 sharp turn radius (override any line follower)
Each Cycle: Enforce motor speeds via wheel_speeds(30, 25)
Exit Condition: Line found with centroid ≥ 0.2 (line re-acquired on right)
On Exit:
Enable line follower: setpoint = 75 mm/s
Set feed-forward = 0.3 (gentle assistance)
Reset distance reference
S9 (Long Straight Segment, Prepare U-Turn)¶
Entry: From S8
Behavior: Continue line following on long segment
Exit Condition: segmentDistance ≥ 1800 mm
On Exit:
Disable line follower
Set motors for U-turn:
wheel_speeds(-50, 50)(left negative, right positive)Creates rotation in place
Reset distance reference
S10 (Perform U-Turn)¶
Entry: From S9
Behavior: Execute U-turn via arc motion
Calculate Arc Distance:
arc_angle = current_heading - start_heading
arc_distance = arc_angle × EFFECTIVE_RADIUS
Exit Condition: segmentDistance ≥ 167 mm (arc distance completed)
On Exit:
Set motors forward:
left = right = 75mm/sReset distance reference
S11 (Resume Line Following After U-Turn)¶
Entry: From S10
Behavior: Move forward slowly, waiting for line re-detection
Exit Condition: Line detected (lineFound = True)
On Exit:
Enable line follower: setpoint = 75 mm/s, Kff = 0.3
Reset distance reference
S12 (Short Radius Curve After Line Recovery)¶
Entry: From S11
Behavior: Line follow for at least 25 mm before checking for line loss (avoid immediate re-loss oscillation)
Exit Condition: segmentDistance ≥ 25 mm AND lineFound = False
On Exit:
Disable line follower & feed-forward
Set motors for R200 radius:
wheel_speeds(200, 50)Reset distance reference
S13 (Final Approach to Checkpoint)¶
Entry: From S12
Behavior: Maintain R200 radius curve toward finish
Exit Condition: segmentDistance ≥ 50 mm
On Exit:
Set motors:
left = right = 50mm/s (slow forward)Reset distance reference
S14 (Final Segment to Finish)¶
Entry: From S13
Behavior: Final forward motion to cross finish line
Exit Condition: segmentDistance ≥ 100 mm
On Exit:
Set
competitionGoFlag = 0(stop race)Return to S0
Distance Tracking & Reset Pattern¶
All segments use the same distance calculation:
segmentDistance = observerCenterDistance.get() - _centerStartDist
At each state transition, _centerStartDist is updated to the current center distance, effectively resetting the segment counter.
Subsystem Activation¶
Subsystem |
S0 |
S1 |
S2–S14 |
|---|---|---|---|
Reflectance |
0 |
3 |
3 (varies) |
Observer |
0 |
1 |
1 |
Motors |
0 |
1 |
1 |
Line Follower |
0 |
1 |
varies |