Sky Juggler

A mechatronics carnival game that challenges users to keep 3 falling aircraft airborne for 60 seconds by aiming at each one with a laser. Final project website found at this link.

Course
ME218A: Smart Product Design Fundamentals

Timeframe

2 weeks [Co-term Fall]

Skills

Event-driven embedded C software for PIC32 microcontroller, electrical circuit design, 3D printing, laser cutting

Game Requirements

  • Power up into a welcoming mode

  • Time out after 20 seconds of no user interaction

  • 60 seconds of gameplay

  • Display the passage of time

  • Hold 50 gears, dispense 1-3 when gameplay is complete

  • 3 distinct user interactions

    • At least one form of non-sensing contact (IR, audio, etc)

    • At least one analog input (IR, joystick, potentiometer, etc)

  • 3 distinct modes of feedback

  • At least one electromechanical actuation (servo motor, fan, vibration, etc)

Inputs

  • Sliding Potentiometer - Adjusts difficulty (falling speed)

  • IR Beam Break Sensor - Card swipe triggers start of game

  • Photocell - Direct laser hit triggers aircraft ascent

Outputs

  • Aircraft Servo Motors - Direction and speed dependent on laser hit and difficulty

  • Gear Dispensing Servo Motor - Dispenses two gears

  • LED Display - Difficulty, countdown, and score

  • Neopixels - Colors change when difficulty adjusted

Instructions

  1. Adjust difficulty using sliding potentiometer

  2. Swipe card to start game

  3. Point laser at photocell centered on each aircraft to raise it

  4. There's a danger line—for every second that an aircraft stays above it, you get a point

  5. If an aircraft crashes in under 60 seconds, you lose and restart

  6. If all 3 aircraft stay airborne for 60s, you win and receive a score!

  7. Either way, you get two prize gears for effort :)


Game Demo

Winning

Losing

Laser Diode (Player Laser)

The laser diode module is a 5 mW, 650 nm red laser used to create a narrow, visible beam for precisely indicating the target location in the game. It operates from a 2.8 V–5.2 V DC supply and draws approximately 25 mA, making it compatible with standard low-voltage rails in the system. The module’s compact cylindrical housing allows it to be rigidly mounted so the beam remains aligned with the target. The laser diode provided a stable visual aiming reference for players. When the beam is directed onto an Adafruit light sensor breakout (phototransistor-based), it causes a measurable change in the sensor’s output voltage, which is read by the PIC32 to detect when a target object is successfully aimed at or “hit.”

Analog Light Sensor (Laser Detector)

The analog light sensor breakout consists of a phototransistor that outputs a voltage proportional to the intensity of incident light. Each of the three sensors was mounted at the center of the target objects in the game, allowing it to detect illumination from the laser diode. The sensor operates from a 3.3 V–5 V supply, with a common ground shared with the microcontroller, and produces an analog voltage that varies with light intensity. The PIC32 reads the detected voltage changes when the laser diode beam strikes the target. 


Mechanical

Full Assembly

The full assembly was designed in Fusion 360. The frame was made out of 0.125" clear laser-cut acrylic, held together by 8-32 x ½" screws and a 0.015" interference between the teeth for a slight press fit. 

Pulley Sub-System

The idle and driver pulley wheels engage with the ridges on the timing belt to vertically translate the aircraft. ¼" ID bearings are press-fit into the idle pulley wheels for low-friction rotation about the ¼" diameter aluminum rod. Each idle pulley wheel is split into two halves to reduce print time, with the halves fastened together with 8-32 x ⅜" screws. The driver pulley wheels are rigidly attached to the servo hub via M4 screws. The pulley wheels were 3D-printed in white PLA. The aluminum rod, timing belts, and servos were purchased off-the-shelf components.

Flight Yoke

The flight yoke provides pitch and yaw control via a pan-tilt mount. Pitch is enabled via a horizontal ¼" x 2" D-shaft, while yaw is enabled by a vertical ¼" x 6" D-shaft that is press fit into the pan-tilt mount. The body is split into two halves, allowing the laser to be inserted when the front panel is removed. A pass-through hole routes the laser's power and ground wires through the mount toward the breadboard in the final assembly. The flight yoke was 3D-printed with black PLA.

Gear Dispenser

The arm mounted on the servo horn swings out 90 degrees and displaces two laser-cut gears simultaneously. The column that holds the gears in place is a separate component that slots into an opening in the ramp body. All components were 3D-printed in white PLA.

Aircraft Mounts

The 3D-printed mounts align with the grooves in the timing belt, allowing them to simply slide in without the need for adhesives. The laser-cut aircraft were secured to each mount with acrylic cement, and a hole was cut to press-fit the photocell into place.  The mounts were 3D-printed with black PLA. The blimp, hot air balloon, and satellite were laser-cut on neon yellow, neon pink, and neon orange acrylic respectively.

Card Reader

The card reader houses an IR beam-break sensor, which is triggered by a black acrylic card. Note: Transparent or non-black solid-colored cards did not reliably activate the sensor. The card reader was 3D-printed with black PLA.

Engineering Drawings


Electrical

Circuit Diagram

Electrical System Overview

The system is made up of one PIC32MX170F256B (PIC32) microcontroller. All the subcomponents are controlled by the singular PIC32. At power on, a singular LED matrix displays "Welcome" and waits for the user to start the game. 

After powering on the game, the player sets the difficulty level (through a potentiometer) and starts the game by swiping a card (through an infrared break beam sensor). NeoPixel lights that are wrapped around the base of the game box change color as the difficulty level is changed.

Three objects (balloon, blimp, and satellite) attached to three separate servos with pulleys begin descending when the game starts. The players can raise the objects by shining the laser diode inside the 3D printed airplane yoke at the center of each object, which each house an analog light sensor. 

When the timer runs out, regardless if the player wins or loses, two gears are dispensed using another motor servo.  

Due to time constraints, we did not transfer the breadboard circuit onto a protoboard, but many measures were taken to ensure the wiring was as robust and decluttered as possible. Wires or breakout pins of each subcomponent were permanently elongated with solder and heatshrink or electric tape, and the power and ground of each component were connected with twisted pair cable. Red and white twisted pair cables were used for 3.3V connections, and green and white twisted pair cables were used for 5V connections to reduce confusion and loose wiring.

Finally, two wires connected external power to the box internal electronics. The 3.3V source plugged into the power source located on the breadboard, where all of the rails were made 3.3V. The 5V source came from an external 5V regulated power supply which connected to a terminal block that was housed inside the game box. All 3.3V power electronics were connected to the breadboard, and all 5V power electronics were connected to the terminal block.

Dot LED Matrix (Display)

The MAX7219 Dot Matrix Module connects to both the PIC32 microcontroller and the 74ACT244 buffer, which increases the signal voltage from 3.3V out of the PIC32 to 5V as required by the LED dot display.

Interactive View

Potentiometer (Difficulty Slider)

The slide potentiometer serves as an analog input device for user-controlled position or value adjustment. It forms a variable voltage divider, outputting a continuous analog voltage proportional to the slider’s position. This voltage is read by the PIC32’s ADC (Analog-to-Digital Converter), allowing the microcontroller to interpret the slider’s position for real-time control or parameter tuning within the system. The 10 kΩ resistance provides stable, low-noise analog signals while minimizing current draw.

NeoPixels

The NeoPixel strip consists of 60 RGB LEDs with integrated driver ICs, allowing each LED to be individually addressed along a single data line. The strip operates from a regulated 5 V supply, with a common ground shared with the PIC32 microcontroller. The PIC32 sends a timing-specific PWM data signal that encodes color and brightness information for each LED. This configuration enables complex lighting patterns using only one control pin while minimizing external circuitry and preserving consistent LED performance across the strip.

IR Beam Break Sensor (Card Reader)

The IR break-beam sensor uses an infrared emitter and a corresponding receiver placed opposite each other. The emitter continuously sends an invisible IR beam; when an object interrupts that beam, the receiver detects the interruption and changes its output. The receiver’s output is a digital signal (open-collector) that goes LOW when the beam is broken and HIGH when the beam is intact. The sensor can be powered from 3.3 V to 5 V (5 V giving the best range, up to ~50 cm), with common ground shared with the microcontroller. A pull-up resistor (internal or external ~10 kΩ) is required on the signal line so the microcontroller reads a clean digital HIGH/LOW.

Servo Motor (Gear Dispenser)

The HS-318 is a standard-size servo motor providing a controlled rotational shaft output, used to actuate the gear dispenser at the end of the game. It operates over a 4.8 V–6.0 V supply range, driven by a PWM control signal from the microcontroller.

Internally, the servo contains a DC motor, a small potentiometer for feedback, and nylon (resin) gears — together forming a closed-loop mechanism that positions the output shaft precisely according to the pulse-width of the control signal. When commanded, the PIC32 sends a periodic pulse with the pulse width specifying the target angle. The servo’s internal controller drives the motor until the internal potentiometer matches the desired shaft position — enabling reliable, repeatable movement of the gear dispenser.

The HS-318 has moderate torque (≈ 3.0–3.7 kg·cm) and speed (~0.15–0.19 sec/60°) and is suitable for actuating small mechanical loads like a gear dispense.

Servo Motor (Pulley System)

The 2000-Series Dual-Mode servo is a standard-sized servo that can operate in two different modes: 1) Default mode (positional mode), which accepts a PWM control signal, directly determining the angular position of the output shaft, and 2) Continuous-rotation mode, in which PWM controls rotational speed rather than absolute position. This mode allows for dynamic motion or repeated up/down cycles if needed.

Key electrical/mechanical characteristics: the servo operates over a supply voltage range of roughly 4.8 V to 7.4 V. Supporting hardware includes steel gears, dual ball-bearing shaft support, and a 25-tooth spline output. These servos were used to drive the up/down motion of three separate targets via pulley systems — leveraging their positional control for precise placement of targets.

Software

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 here.

GameSM

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

MotorCtrl

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

LEDService

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


Gallery


Previous
Previous

CS230: DeepSea