Week 10

Output Devices

pitch timer running — Nextion display showing 05:00, WS2812B LED strip glowing green — ESAN Fab Lab
Fab Academy

Output Devices — Programming the pitch timer: Nextion Display, LED Strip and Buzzer


Group assignment
Measure the power consumption of an output device. Document your work on the group work page and reflect on your individual page what you learned.

Individual assignment
Add an output device to a microcontroller board you've designed and program it to do something.


What I knew beforehand

Before this week I had some understanding of how output devices work conceptually — LEDs signal state, displays communicate information, buzzers provide audio feedback. What I had not done before was program all three simultaneously on a single microcontroller board, with each device responding in real time to user input from a touch screen.

An output device is the interface between a system and its user. The quality of that interface determines whether the system is legible — whether a person can understand what it is doing without reading a manual. That framing shaped every design decision I made this week, from the color logic of the LED strip to the layout of the Nextion touch screen.

This documentation was produced in June 2026, during my second and Fab Academy cycle, and represents entirely new work. Programming three output devices simultaneously — the Nextion HMI touch display, the WS2812B LED strip, and the passive buzzer — on a single XIAO RP2040 using a non-blocking millis() state machine was the most complex firmware challenge of my Fab Academy experience. The traffic-light color logic for the LED strip (green → yellow → red as time runs down) was a deliberate design choice rooted in an entrepreneurship principle: a good interface borrows a mental model the user already has, rather than creating a new one. PitchLight's output devices are the only part of the system a presenter ever perceives; everything else is infrastructure.

Group assignment

I worked with my colleague Jhasmin Ayala

Check this link

Individual Assignment — Pitch timer Output Devices

For this assignment I programmed my board — the custom PCB I milled in Week 8 — to control three output devices simultaneously:

  • Nextion NX4832T035_011 HMI display — 3.5" touchscreen, UART communication at 9600 baud, shows the countdown timer and receives touch events
  • WS2812B NeoPixel LED strip — 16 addressable RGB LEDs, single-wire NeoPixel protocol on D0, color changes according to time remaining
  • Passive buzzer — PWM tone at 2 kHz on D1, 5-second alert when countdown reaches zero

pitch timer system connection diagram — XIAO RP2040 connected to Nextion display via UART, NeoPixel strip on D1, buzzer on D3

System connection diagram for pitch timer. The XIAO RP2040 microcontroller (center) communicates with the Nextion NX4832T035 display via UART at 9600 baud (D6/TX → display RX, D7/RX → display TX). The WS2812B NeoPixel strip (8 LEDs) is driven from D1. The passive buzzer is controlled via PWM on D3. All devices share a common 5V power rail and GND.


Part 1 — Programming the Nextion HMI Display

Nextion displays are programmed using Nextion Editor, a dedicated GUI design tool. Unlike a regular display driver, Nextion runs its own processor — the XIAO only needs to send UART commands to update values on screen and receive events when buttons are touched. The display handles all rendering internally.

Download: nextion.tech/nextion-editor

Step 1 — Create a New Project: Select Device Model and Orientation

1Setting → Device → Select model: NX4832T035_011

In Nextion Editor I created a new project and opened Setting → Device to select the correct display model. I chose NX4832T035_011 — 3.5 inches, 320×480 pixels, 16 MB Flash, 3584 B RAM, 48 MHz. Selecting the wrong model causes the compiled .tft file to be rejected by the hardware.
Nextion Editor — Setting dialog showing device model selection with NX4832T035_011 highlighted in pink

Nextion Editor — Setting → Device panel. The model NX4832T035_011 is selected (highlighted in pink): 3.5 inch, 320×480 px, Flash 16M, RAM 3584B, Frequency 48M. Selecting the correct model is essential — the .tft output file is hardware-specific and will not load on a different model.

2Setting → Display → Direction: 90° (Horizontal)

Under the Display tab I set the orientation to 90° — Horizontal. This matches the physical orientation of the display in the pitch timer enclosure, where the longer dimension runs left to right. Character encoding was left at iso-8859-1.
Nextion Editor — Setting Display tab showing 90 Horizontal selected and iso-8859-1 character encoding

Nextion Editor — Setting → Display tab. Display direction set to 90° — Horizontal (highlighted in orange). Character encoding: iso-8859-1. These settings define the canvas dimensions the editor will use and how the display will render in its physical position.

Step 2 — Design the UI: Background Images and Components

The Nextion UI is built by adding background images and placing interactive components on top. Two background images are required: one for the default state (buttons not pressed) and one for the pressed state (button highlighted). Each image must be resized to exactly 480 × 320 pixels to fill the horizontal canvas.

The default state background shows the PITCH TIMER title bar, three buttons (START in green, PAUSE in yellow, RESET in red), and two arrow buttons for preset selection. The pressed-state background shows the same layout with darker button colors to simulate physical feedback.

Nextion Editor UI — default state: PITCH TIMER title, START/PAUSE/RESET buttons, up/down arrows for preset

Default state background (480 × 320 px). Title bar: PITCH TIMER. Three action buttons: START (green), PAUSE (yellow), RESET (red). Two arrow buttons (▲ / ▼) on the left for changing the timer preset between 3, 4, and 5 minutes. The black area at center-right is where the countdown text component (t0) will render.

Nextion Editor UI — pressed state: same layout with slightly different button shading for visual feedback

Pressed state background (480 × 320 px) — same layout with adjusted button shading. Nextion uses two background images to simulate button press visual feedback: the editor swaps between the two backgrounds when the touch event fires. This gives the UI a tactile, responsive feel without requiring individual button state images.

Step 3 — Add Interactive Components

With the backgrounds in place, I added the following components in the Nextion Editor canvas:

3aText component — countdown display (objname: t0)

From the Toolbox panel I selected Text (T icon) and drew a large rectangle in the center of the canvas. Attributes set in the Attribute panel: objname: t0 · txt: 05:00 · pco: 65535 (white text) · bco: 0 (black background) · xcen: 1 (horizontal center) · ycen: 1 (vertical center). This component is updated every second by the XIAO via UART command t0.txt="MM:SS" followed by three 0xFF bytes.
3bButton components — START, PAUSE, RESET

I added three Button components from the Toolbox and positioned them to align with the button images in the background. For each button I set the object name, text, and a Touch Release event that sends a text command over UART to the XIAO RP2040:

ButtonobjnameTouch Release event
STARTbStartprint "START" · printh 0a
PAUSEbPauseprint "PAUSE" · printh 0a
RESETbResetprint "RESET" · printh 0a

The printh 0a command sends a newline byte (0x0A) as a command terminator so the XIAO firmware can detect the end of each command string.
3cPreset arrows — variable vPreset, label t1, buttons bUp and bDown

To allow the user to change the timer duration (3, 4, or 5 minutes) I added:

  • Variable componentobjname: vPreset, val: 5 (default 5 minutes)
  • Text labelobjname: t1, txt: 5 min — positioned between the two arrows
  • Button bDown (decrease) — Touch Release event cycles: 5→4→3, sends print "PRESET:4" or print "PRESET:3" + printh 0a
  • Button bUp (increase) — Touch Release event cycles: 3→4→5, sends print "PRESET:4" or print "PRESET:5" + printh 0a
Each arrow event also updates vPreset.val and t1.txt locally on the display, so no UART round-trip is needed to update the preset label.

Step 4 — Compile and Upload to the Nextion Display

4File → TFT File Output → copy to microSD → insert and power on

Once all components and events were set, I compiled the project and uploaded it to the physical display using a microSD card:

  1. File → TFT File Output — select output folder. This generates a .tft binary file containing the compiled UI.
  2. Copy the .tft file to a microSD card formatted as FAT32.
  3. Insert the SD card into the Nextion display slot while the display is powered off.
  4. Power on the Nextion — it detects the SD card and loads the .tft file automatically. A progress bar is shown during flashing.
  5. When flashing completes, power off, remove the SD card, and power on again. The UI is now running from internal flash.

Part 2 — XIAO RP2040 Firmware

With the Nextion UI flashed and running, I wrote the Arduino firmware for the XIAO RP2040 that coordinates all three output devices using a non-blocking state machine driven by millis().

The firmware listens for UART commands from the Nextion display, updates the countdown every second, changes the LED strip color based on remaining time, and triggers the buzzer when the timer reaches zero — all simultaneously, without any blocking delay() calls.

Output DeviceProtocolXIAO PinFunction
Nextion NX4832T035_011 displayUART — Serial1, 9600 baudD6 (TX) / D7 (RX)Sends MM:SS countdown; receives START / PAUSE / RESET / PRESET:n commands
WS2812B LED strip — 16 LEDsNeoPixel single-wireD0Green → Yellow → Red as time decreases; solid Red at finish
Passive buzzerPWM tone — 2 kHzD15-second alert tone when countdown reaches zero

LED color logic:

ConditionLED ColorRGB value
Remaining > 20% of selected duration🟢 Green(0, 255, 0)
Remaining ≤ 20% of selected duration🟡 Yellow(255, 180, 0)
Remaining ≤ 10 seconds🔴 Red(255, 0, 0)
Timer finished🔴 Red + buzzer(255, 0, 0) + tone(2000 Hz, 5 s)

The complete firmware:

// pitch timer firmware — XIAO RP2040 // Output devices: Nextion HMI (UART), WS2812B LED strip (NeoPixel), Passive buzzer (PWM) #include <Adafruit_NeoPixel.h> // ── PIN CONFIGURATION ────────────────────────────────────── #define LED_PIN D0 #define LED_COUNT 16 #define BUZZER_PIN D1 #define NEXTION_SERIAL Serial1 // TX=D6, RX=D7 Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); // ── TIMER STATE MACHINE ──────────────────────────────────── enum TimerState { STOPPED, RUNNING, PAUSED, FINISHED }; TimerState timerState = STOPPED; const uint32_t PRESET_5_MIN = 300; const uint32_t PRESET_4_MIN = 240; const uint32_t PRESET_3_MIN = 180; uint32_t selectedDuration = PRESET_5_MIN; uint32_t remainingSeconds = PRESET_5_MIN; unsigned long lastTickMs = 0; bool buzzerActive = false; unsigned long buzzerStartMs = 0; String rxLine = ""; // ── NEXTION HELPERS ─────────────────────────────────────── void nextionEnd() { NEXTION_SERIAL.write(0xFF); NEXTION_SERIAL.write(0xFF); NEXTION_SERIAL.write(0xFF); } void nextionCommand(const String &cmd) { NEXTION_SERIAL.print(cmd); nextionEnd(); } void updateDisplayTime(uint32_t totalSec) { char buf[10]; sprintf(buf, "%02lu:%02lu", totalSec/60, totalSec%60); nextionCommand("t0.txt=\"" + String(buf) + "\""); } // ── LED HELPERS ─────────────────────────────────────────── void setAllPixels(uint8_t r, uint8_t g, uint8_t b) { for (int i = 0; i < LED_COUNT; i++) strip.setPixelColor(i, strip.Color(r, g, b)); strip.show(); } void updateLedByTime() { if (remainingSeconds <= 10) setAllPixels(255, 0, 0); // RED else if (remainingSeconds <= (selectedDuration * 20UL) / 100UL) setAllPixels(255, 180, 0); // YELLOW else setAllPixels(0, 255, 0); // GREEN } // ── TIMER CONTROL ───────────────────────────────────────── void startTimer() { if(remainingSeconds==0) remainingSeconds=selectedDuration; timerState=RUNNING; lastTickMs=millis(); updateLedByTime(); } void pauseTimer() { if(timerState==RUNNING) timerState=PAUSED; } void resetTimer() { timerState=STOPPED; remainingSeconds=selectedDuration; updateDisplayTime(remainingSeconds); updateLedByTime(); } void finishTimer() { timerState=FINISHED; remainingSeconds=0; updateDisplayTime(0); setAllPixels(255,0,0); buzzerActive=true; buzzerStartMs=millis(); tone(BUZZER_PIN, 2000); // 2 kHz alert } void setPreset(uint8_t min) { switch(min) { case 5: selectedDuration=PRESET_5_MIN; break; case 4: selectedDuration=PRESET_4_MIN; break; case 3: selectedDuration=PRESET_3_MIN; break; default: return; } timerState=STOPPED; remainingSeconds=selectedDuration; updateDisplayTime(remainingSeconds); updateLedByTime(); } // ── NEXTION UART PARSER ─────────────────────────────────── void processCommand(String cmd) { cmd.trim(); if (cmd == "START") startTimer(); else if (cmd == "PAUSE") pauseTimer(); else if (cmd == "RESET") resetTimer(); else if (cmd.startsWith("PRESET:")) setPreset(cmd.substring(7).toInt()); } void readNextionSerial() { while (NEXTION_SERIAL.available()) { char c = NEXTION_SERIAL.read(); if ((uint8_t)c == 0xFF) continue; if (c == '\n' || c == '\r') { if(rxLine.length()>0){ processCommand(rxLine); rxLine=""; } } else { rxLine += c; if (rxLine=="START"||rxLine=="PAUSE"||rxLine=="RESET"||rxLine=="PRESET:5"||rxLine=="PRESET:4"||rxLine=="PRESET:3") { processCommand(rxLine); rxLine=""; } } } } // ── SETUP ───────────────────────────────────────────────── void setup() { pinMode(BUZZER_PIN, OUTPUT); noTone(BUZZER_PIN); Serial.begin(115200); NEXTION_SERIAL.begin(9600); strip.begin(); strip.show(); selectedDuration = PRESET_5_MIN; remainingSeconds = selectedDuration; delay(500); updateDisplayTime(remainingSeconds); updateLedByTime(); } // ── LOOP ────────────────────────────────────────────────── void loop() { readNextionSerial(); unsigned long now = millis(); if (timerState == RUNNING && now - lastTickMs >= 1000) { lastTickMs += 1000; if (remainingSeconds > 0) { remainingSeconds--; updateDisplayTime(remainingSeconds); updateLedByTime(); } if (remainingSeconds == 0) finishTimer(); } if (buzzerActive && now - buzzerStartMs >= 5000) { noTone(BUZZER_PIN); buzzerActive = false; } }

Result — pitch timer Running

With the Nextion UI flashed to the display and the firmware uploaded to the XIAO RP2040, the complete pitch timer system ran correctly on first boot. The display showed 05:00, the LED strip lit up green, and all three touch buttons responded immediately.

pitch timer fully operational — Nextion display showing PITCH TIMER 05:00, WS2812B LED strip glowing green, milled PCB with XIAO RP2040

pitch timer fully operational. The Nextion 3.5" HMI touch display (bottom) shows PITCH TIMER with START, PAUSE, and RESET buttons and the 05:00 countdown. The milled PCB with XIAO RP2040 (top) connects to the display via UART and drives the WS2812B LED strip — glowing green, indicating full time remaining. The purple wire connects the passive buzzer.


Reflections

This week crystallized something I had understood theoretically but not yet experienced in practice: the output devices are the only part of an embedded system that a user ever directly perceives. Everything else — the microcontroller, the firmware logic, the PCB traces — is invisible infrastructure. The quality of the interface is what determines whether the system is useful.

Designing the Nextion UI forced me to think about the presentation as a whole experience, not just a technical output. The three-color LED system — green, yellow, red — does not require explanation. It maps onto a cognitive model that everyone already has from traffic lights. That is good interface design: it borrows an existing mental model rather than creating a new one. From an entrepreneurship perspective, this is exactly what a pitch timer for first-time presenters needs: a signal that is legible at a glance, without diverting attention from the audience.


Resources

final_project_hmi.tft — Nextion compiled UI file

Licensed under CC BY-NC-SA 4.0 — © Marita Chang