State Diagram
Code
This section outlines the three services we wrote and consist of a top level Game state machine, a Motor control service and an LED service. The entire code repository can be found at (link).
The GameSM controls the main actions of the overall game. This service keeps track of the different states and posts events to the LEDService and MotorCtrl. Game FSM will also handle gameplay and user inactivity timers. There is also a testing mode built in which was used for debugging.
DEFINE STATES:
GS_InitPState, GS_WaitingForHandWave, GS_Gameplay,
GS_NoUserInput, GS_LosingMode, GS_CompletingMode,
GS_TestMode
GLOBAL VARIABLES:
CurrentState // current game state
SecondsLeft // remaining game time (0–60 s)
g_Score // accumulated score over one game
MyPriority // service priority in ES framework
INITIALIZE HARDWARE (helper):
FUNCTION GameHW_InitPins():
CONFIGURE beam break pin as digital input with pull-up
CONFIGURE slider pin (AN11) as analog input
CONFIGURE 3 ALS sensor pins (AN12, AN5, AN4) as analog inputs
CONFIGURE ADC auto-scan for AN4, AN5, AN11, AN12
CAPTURE BASELINES (helper):
FUNCTION CaptureALS_Baselines_Init():
FOR i = 1..N samples:
READ ADC_MultiRead into array adc[]
EXTRACT an4, an5, an12 channels from adc[]
ACCUMULATE sums for each channel
COMPUTE average baseline for each channel
CALL Targets_SetBaselines(baseline_AN12, baseline_AN5, baseline_AN4)
FUNCTION InitGameSM(Priority):
SET MyPriority = Priority
CALL GameHW_InitPins()
IF START_IN_TEST_MODE is defined:
SET CurrentState = GS_TestMode
ELSE:
SET CurrentState = GS_InitPState
ENDIF
POST Event: ES_INIT to this service
RETURN success or failure
FUNCTION PostGameSM(Event):
POST Event to this state machine's queue
RETURN success or failure
FUNCTION RunGameSM(Event):
EventType = Event.EventType
SWITCH CurrentState:
CASE GS_InitPState:
IF EventType == ES_INIT:
CALL CaptureALS_Baselines_Init() // average ALS sensors at boot
POST Event ES_LED_SHOW_MESSAGE with param LED_MSG_WELCOME to LEDService
CALL MC_RaiseAllToTop() // move all balloons to top
SET CurrentState = GS_WaitingForHandWave
ENDIF
CASE GS_WaitingForHandWave:
SWITCH EventType:
CASE ES_DIFFICULTY_CHANGED:
pct = Event.EventParam // 0–100%
POST Event ES_LED_SHOW_DIFFICULTY(pct) to LEDService
CALL MC_SetDifficultyPercent(pct) // adjust balloon speed
CASE ES_HAND_WAVE_DETECTED:
g_Score = 0
SecondsLeft = 60
POST Event ES_LED_SHOW_COUNTDOWN(SecondsLeft) to LEDService
START Timer(TID_GAME_60S, 60 000 ms)
START Timer(TID_INACTIVITY_20S, 20 000 ms)
START Timer(TID_TICK_1S, 1 000 ms)
CALL MC_CommandFall(1) // all balloons start falling
CALL MC_CommandFall(2)
CALL MC_CommandFall(3)
SET CurrentState = GS_Gameplay
END SWITCH
CASE GS_Gameplay:
SWITCH EventType:
// Laser hit events – when hit, balloon should rise
CASE DIRECT_HIT_B1:
CALL MC_CommandRise(1)
RESTART Timer(TID_INACTIVITY_20S, 20 000 ms)
CASE DIRECT_HIT_B2:
CALL MC_CommandRise(2)
RESTART Timer(TID_INACTIVITY_20S, 20 000 ms)
CASE DIRECT_HIT_B3:
CALL MC_CommandRise(3)
RESTART Timer(TID_INACTIVITY_20S, 20 000 ms)
// When no hit, balloon should fall
CASE NO_HIT_B1:
CALL MC_CommandFall(1)
CASE NO_HIT_B2:
CALL MC_CommandFall(2)
CASE NO_HIT_B3:
CALL MC_CommandFall(3)
// Timer events
CASE ES_TIMEOUT:
IF Event.EventParam == TID_TICK_1S:
IF SecondsLeft > 0:
DECREMENT SecondsLeft
ENDIF
POST ES_LED_SHOW_COUNTDOWN(SecondsLeft) to LEDService
afloat = MC_CountBalloonsAboveDangerline()
g_Score = g_Score + afloat // scoring: add #safe balloons each second
RESTART Timer(TID_TICK_1S, 1000 ms)
ELSE IF Event.EventParam == TID_GAME_60S:
// Game time finished – victory path
SET CurrentState = GS_CompletingMode
POST ES_LED_SHOW_SCORE(g_Score) to LEDService
START Timer(TID_MODE_3S, 3000 ms)
ELSE IF Event.EventParam == TID_INACTIVITY_20S:
// User did nothing for 20 s – inactivity timeout
SET CurrentState = GS_NoUserInput
START Timer(TID_MODE_3S, 3000 ms)
ENDIF
CASE ES_OBJECT_CRASHED:
// Any balloon hit floor – losing condition
SET CurrentState = GS_LosingMode
POST ES_LED_SHOW_SCORE(g_Score) to LEDService
START Timer(TID_MODE_3S, 3000 ms)
END SWITCH
CASE GS_NoUserInput:
IF EventType == ES_TIMEOUT AND Event.EventParam == TID_MODE_3S:
CALL MC_RaiseAllToTop()
POST ES_LED_SHOW_MESSAGE(LED_MSG_WELCOME) to LEDService
SET CurrentState = GS_WaitingForHandWave
ENDIF
CASE GS_LosingMode:
IF EventType == ES_TIMEOUT AND Event.EventParam == TID_MODE_3S:
CALL MC_RaiseAllToTop()
CALL MC_DispenseTwoGearsOnce() // dispense prize gears
POST ES_LED_SHOW_MESSAGE(LED_MSG_WELCOME) to LEDService
SET CurrentState = GS_WaitingForHandWave
ENDIF
CASE GS_CompletingMode:
IF EventType == ES_TIMEOUT AND Event.EventParam == TID_MODE_3S:
CALL MC_RaiseAllToTop()
CALL MC_DispenseTwoGearsOnce() // dispense prize gears
POST ES_LED_SHOW_MESSAGE(LED_MSG_WELCOME) to LEDService
SET CurrentState = GS_WaitingForHandWave
ENDIF
CASE GS_TestMode:
// Calibration / debug mode driven by keyboard events
// Stop balloon update timer so we can manually move servos
STOP Timer(TID_BALLOON_UPDATE)
IF EventType == ES_NEW_KEY:
k = (char)Event.EventParam
SWITCH k:
CASE '1':
CALL CaptureALS_Baselines_Init()
CASE '2':
PRINT current beam-break input value
CASE '3':
CALL MC_RaiseAllToTop()
CASE 'm':
TEST single servo by setting PWM to a debug pulse width
CASE 'a':
READ ADC_MultiRead
PRINT slider and 3 ALS values
CASE '8':
CALL MC_CommandRise(1)
CASE 'q':
CALL MC_CommandFall(1)
CASE '9':
CALL MC_CommandRise(2)
CASE 'w':
CALL MC_CommandFall(2)
CASE 'f':
CALL MC_CommandRise(3)
CASE 'e':
CALL MC_CommandFall(3)
CASE 'g':
CALL MC_DispenseTwoGearsOnce()
CASE 'd':
CALL MC_DebugPrintAxes()
CASE 'l':
POST ES_LED_SHOW_MESSAGE(LED_MSG_WELCOME) to LEDService
CASE 'x':
POST ES_LED_SHOW_MESSAGE(LED_MSG_WELCOME) to LEDService
SET CurrentState = GS_WaitingForHandWave
START Timer(TID_BALLOON_UPDATE) // restart automatic motor control
END SWITCH
ENDIF
END SWITCH
RETURN ES_NO_EVENT (or ES_ERROR on errors)
FUNCTION QueryGameFSM():
RETURN CurrentState
This service is responsible for commanding the motors for each balloon as well as the gear dispenser.
DATA STRUCTURES:
STRUCT Axis_t:
pos_ticks // current servo position in timer ticks
tgt_ticks // target position in ticks
max_step // max change per update frame (speed)
floor_ticks // calibrated bottom position (balloon crashed)
ceiling_ticks // calibrated top position
Ax[3] // one Axis_t per balloon (B1, B2, B3)
chan[3] // servo channel mapping for B1, B2, B3
g_crashed // flag: has any balloon already reported a crash?
FUNCTION MotorHW_InitServos():
CONFIGURE PWM library with 5 channels
ASSIGN servo channels to Timer3 at 50 Hz:
Channel 1 -> gear servo (OC1)
Channel 3 -> B1 (OC3)
Channel 4 -> B2 (OC4)
Channel 5 -> B3 (OC5)
MAP PWM channels to pins:
OC1 -> RPB15 (gear)
OC3 -> RPA3 (B1)
OC4 -> RPA4 (B2)
OC5 -> RPA2 (B3)
FUNCTION InitMotorCtrl(Priority):
SET MyPriority = Priority
START Timer(TID_BALLOON_UPDATE, 100 ms) // servo update period
CALL MotorHW_InitServos()
FOR each balloon i in {0,1,2}:
SET Ax[i].ceiling_ticks = calibrated MIN_TICKS for that servo
SET Ax[i].floor_ticks = calibrated MAX_TICKS for that servo
SET Ax[i].pos_ticks = ceiling_ticks (start at top)
SET Ax[i].tgt_ticks = ceiling_ticks
SET Ax[i].max_step = 50 ticks (temporary; overwritten later)
POST Event ES_INIT to this service
RETURN success or failure
FUNCTION PostMotorCtrl(Event):
POST Event to this service's queue
RETURN success or failure
FUNCTION RunMotorCtrl(Event):
IF EventType == ES_TIMEOUT AND EventParam == TID_BALLOON_UPDATE:
IF QueryGameSM() == GS_Gameplay:
FOR each balloon i in {0,1,2}:
delta = Ax[i].tgt_ticks - Ax[i].pos_ticks
LIMIT delta to range [−Ax[i].max_step, +Ax[i].max_step]
Ax[i].pos_ticks = Ax[i].pos_ticks + delta
// Clamp inside calibrated range
IF Ax[i].pos_ticks > Ax[i].floor_ticks:
Ax[i].pos_ticks = Ax[i].floor_ticks
IF Ax[i].pos_ticks < Ax[i].ceiling_ticks:
Ax[i].pos_ticks = Ax[i].ceiling_ticks
// Drive PWM channel with new position
SET PWM on servo channel chan[i] to Ax[i].pos_ticks ticks
// Crash detection
IF Ax[i].pos_ticks >= Ax[i].floor_ticks AND g_crashed == false:
SET g_crashed = true
POST Event ES_OBJECT_CRASHED to GameSM
RESTART Timer(TID_BALLOON_UPDATE, 100 ms)
RETURN ES_NO_EVENT
IF EventType == ES_TIMEOUT AND EventParam == TID_GEAR_SERVO:
// Timer expired means gear servo is at dispensing position
// Move it back to rest position
SET gear servo PWM to rest position ticks
RETURN ES_NO_EVENT
RETURN ES_NO_EVENT
PUBLIC HELPER FUNCTION MC_SetDifficultyPercent(pct):
MIN_STEP_TICKS = 5 // very easy
MAX_STEP_TICKS = 20 // very hard
// Linearly map pct=1..100 onto [MIN_STEP_TICKS..MAX_STEP_TICKS]
step = MIN_STEP_TICKS + (pct − 1) * (MAX_STEP_TICKS − MIN_STEP_TICKS) / 99
FOR each balloon i:
Ax[i].max_step = step
PUBLIC HELPER FUNCTION MC_DispenseTwoGearsOnce():
SET gear servo PWM to dispensing position ticks
START Timer(TID_GEAR_SERVO, 500 ms) // dwell time before returning to rest
PUBLIC HELPER FUNCTION MC_CommandRise(idx):
Ax[idx − 1].tgt_ticks = Ax[idx − 1].ceiling_ticks // move toward top
PUBLIC HELPER FUNCTION MC_CommandFall(idx):
Ax[idx − 1].tgt_ticks = Ax[idx − 1].floor_ticks // move toward bottom
PUBLIC HELPER FUNCTION MC_RaiseAllToTop():
FOR each balloon i:
Ax[i].pos_ticks = Ax[i].ceiling_ticks
Ax[i].tgt_ticks = Ax[i].ceiling_ticks
SET PWM on chan[i] to ceiling_ticks
g_crashed = false // reset crash status
PUBLIC HELPER FUNCTION MC_DebugPrintAxes():
PRINT debug information for each balloon:
pos_ticks, tgt_ticks, floor_ticks, ceiling_ticks
PUBLIC HELPER FUNCTION MC_CountBalloonsAboveDangerline():
count = 0
FOR each balloon i:
dangerLine = floor_ticks + (ceiling_ticks − floor_ticks)/4 // tunable
IF Ax[i].pos_ticks >= dangerLine:
INCREMENT count
RETURN count
This service is responsible for controlling the LED display as well as the neopixel array we used.
GLOBAL VARIABLES:
MyPriority
g_DisplayInitDone // has MAX7219 init completed?
g_LedPushPending // is there content in buffer to push row-by-row?
LastDifficultyBucket // cached difficulty bucket for NeoPixel coloring
FUNCTION InitLEDService(Priority):
SET MyPriority = Priority
CALL LED_SPI_Init() // configure SPI1 + MAX7219 pins
CALL neopixel_init() // configure NeoPixel output pin
POST Event ES_INIT to this service
RETURN success or failure
FUNCTION PostLEDService(Event):
POST Event to this service's queue
RETURN success or failure
FUNCTION RunLEDService(Event):
SWITCH Event.EventType:
CASE ES_INIT:
IF g_DisplayInitDone == false:
done = DM_TakeInitDisplayStep() // performs one init step
IF done == false:
// Not finished; re-post ES_INIT to continue later
POST ES_INIT to this service again
RETURN ES_NO_EVENT
ELSE:
g_DisplayInitDone = true
ENDIF
CASE ES_LED_SHOW_DIFFICULTY:
pct = (uint8_t)Event.EventParam
CALL LED_RenderDifficulty(pct)
CASE ES_LED_SHOW_COUNTDOWN:
seconds = (uint8_t)Event.EventParam
CALL LED_RenderCountdown(seconds)
CASE ES_LED_SHOW_SCORE:
score = (uint16_t)Event.EventParam
CALL LED_RenderScore(score)
CASE ES_LED_SHOW_MESSAGE:
msgID = (LED_MessageID_t)Event.EventParam
CALL LED_RenderMessage(msgID)
CASE ES_LED_PUSH_STEP:
IF g_LedPushPending == true:
done = DM_TakeDisplayUpdateStep() // push one row to modules
IF done == false:
POST ES_LED_PUSH_STEP again // continue on next dispatch
ELSE:
g_LedPushPending = false // all 8 rows sent
ENDIF
CASE ES_DIFFICULTY_CHANGED:
diffPct = (uint8_t)Event.EventParam
CALL LED_UpdateDifficultyNeopixels(diffPct)
END SWITCH
RETURN ES_NO_EVENT
HELPER FUNCTION LED_SPI_Init():
CONFIGURE SPI1 basic settings via SPI HAL:
master mode, bit time ~100 ns/bit
map SS to RPA0, SDO to RPA1
configure clock polarity/phase
enable 16-bit mode with enhanced buffer
enable SPI1
HELPER FUNCTION LED_RenderDifficulty(pct):
CLAMP pct into [1,100]
FORMAT pct as decimal string into buf[]
CLEAR DM display buffer
FOR each character ch in buf:
ADD ch into DM display buffer
SCROLL display buffer by 4 columns
SET g_LedPushPending = true
POST ES_LED_PUSH_STEP to LEDService
HELPER FUNCTION LED_RenderCountdown(seconds_remaining):
FORMAT seconds_remaining as string into buf[]
CLEAR DM display buffer
FOR each character ch in buf:
ADD ch into DM display buffer
SCROLL display buffer by 4 columns
SET g_LedPushPending = true
POST ES_LED_PUSH_STEP to LEDService
HELPER FUNCTION LED_RenderScore(score):
FORMAT score as decimal string into numBuf[]
CLEAR DM display buffer
PREFIX = "SC:"
FOR each character ch in PREFIX:
ADD ch into DM display buffer
SCROLL display buffer by 4 columns
FOR each character ch in numBuf:
ADD ch into DM display buffer
SCROLL display buffer by 4 columns
SET g_LedPushPending = true
POST ES_LED_PUSH_STEP to LEDService
HELPER FUNCTION LED_RenderMessage(msgID):
IF msgID == LED_MSG_WELCOME:
msg = "WELCOME"
ELSE:
msg = "WELCOME" // default
CLEAR DM display buffer
FOR each character ch in msg:
ADD ch into DM display buffer
SCROLL display buffer by 4 columns
SET g_LedPushPending = true
POST ES_LED_PUSH_STEP to LEDService
HELPER FUNCTION LED_UpdateDifficultyNeopixels(difficultyPercent):
COMPUTE bucket = difficultyPercent / 15 // 0–6
CLAMP bucket to [0,6]
IF bucket == LastDifficultyBucket:
RETURN (no update)
SET LastDifficultyBucket = bucket
CHOOSE (r_full, g_full, b_full) based on bucket:
0 → green
1 → green-ish
2 → yellow-green
3 → yellow
4 → orange
5 → deep orange/red
6 → red
APPLY global brightness scaling to (r,g,b)
CALL neopixel_clear()
FOR each pixel i in 0..NUM_NEOPIXELS−1:
CALL neopixel_set_pixel(i, r, g, b)
CALL neopixel_show() // pushes new color pattern to strip