29  Lab: Interface Design

ux-design
interface
hands
on

29.1 Start Simple

This lab starts with behavior you can observe and record. Treat each run as evidence: what state appeared, what feedback changed, which modality helped or failed, how long the response took, and what the design should do differently next time.

Putting Numbers to It

Lab execution time can be estimated before starting runs:

\[ T_{\text{total}} = N_{\text{runs}} \times (t_{\text{setup}} + t_{\text{run}} + t_{\text{review}}) \]

Worked example: With 5 runs and per-run times of 4 min setup, 6 min execution, and 3 min review, total lab time is \(5\times(4+6+3)=65\) minutes. This prevents under-scoping and helps schedule complete experimental cycles.

Interactive Calculator:

This calculator helps you plan lab sessions realistically. For example, with typical times (4 min setup, 6 min execution, 3 min review) and 5 runs, you need 65 minutes—not the 30 minutes a naive estimate might suggest.

In 60 Seconds

This hands-on lab uses the Wokwi ESP32 simulator to build an accessible IoT interface featuring menu navigation with multimodal feedback (visual OLED display, audio buzzer tones, LED blinks), high-contrast and large-text accessibility modes, and persistent user preferences stored in non-volatile memory – all demonstrating core accessibility design principles without requiring physical hardware.

Chapter Roadmap

This is a long lab, so use this path through it:

  1. First size the lab run and define the evidence you need from each interface test.
  2. Then connect the ESP32 controls, OLED, buzzer, and LED to a menu design question.
  3. Next build the accessible Wokwi interface and verify each feedback channel.
  4. After that compare the lab results against accessibility testing, priority, and retrofit risks.
  5. Finally extend the design with audit, voice-over, auto-repeat, and status-pattern challenges.

Checkpoint callouts recap what you have already proved; longer code and challenge sections are places to slow down, run the simulator, and record evidence before moving on.

29.2 Learning Objectives

By completing this lab, you will be able to:

  • Design Accessible Menus: Build hierarchical navigation that works for users with varying abilities
  • Implement Multimodal Feedback: Configure visual, audio, and tactile confirmation of user actions
  • Apply High Contrast Design: Demonstrate color and contrast patterns that meet WCAG 2.1 accessibility standards
  • Configure User Preference Storage: Implement persistent accessibility settings using non-volatile storage (NVS)
  • Implement Diverse Input Methods: Build button navigation for users who cannot use touchscreens
  • Evaluate Glanceable Displays: Assess whether interfaces are readable at a glance with clear visual hierarchy
Hands-On Interface Lab

This hands-on chapter lets you practice UX design skills for IoT through exercises and real scenarios. Think of it as art class where you actually draw instead of just studying art theory. Working through these design exercises builds practical skills you can apply to your own IoT projects.

Key Concepts

  • Interaction Design: Discipline defining how users communicate with digital systems through input, output, and feedback mechanisms.
  • Multimodal Interface: System accepting input and delivering output through multiple channels (touch, voice, gesture, haptic) simultaneously.
  • User Testing: Structured observation of representative users attempting defined tasks, exposing interface problems invisible to designers.
  • Prototype Fidelity: Level of detail in a prototype: low fidelity (paper sketch) validates concepts; high fidelity (interactive mockup) validates usability.
  • Information Architecture: Structural design of digital spaces to support usability and findability, determining where content lives and how users navigate.
  • Cognitive Load: Mental effort required to use an interface; IoT systems must minimise cognitive load for users managing many connected devices.
  • Usability Heuristic: Principle-based rule for evaluating interface quality (e.g. Nielsen’s 10 heuristics) without requiring user testing.

29.3 Prerequisites

  • Basic understanding of Arduino/C++ syntax
  • Familiarity with interface design principles from previous chapters
  • No physical hardware required (browser-based simulation)

29.4 Lab Tests Feedback

This lab is about checking whether an embedded interface communicates clearly through more than one channel. The ESP32, SSD1306 OLED, navigation buttons, buzzer, status LED, and Wokwi simulator are the testbed. The design question is whether a user can navigate, confirm, recover, and preserve accessibility preferences without relying on one fragile cue.

Treat each run as an interface test. When you press Up, Down, Select, or Back, check the display state, tone pattern, LED blink, serial log, and saved setting. A menu action is not complete until the user can tell what happened, the device state is consistent, and the next action is clear.

Interface modality selection map comparing voice, wearable, physical controls, embedded display, mobile app, and web patterns by user attention and interaction complexity.
Figure 29.1: The lab focuses on the physical-control and embedded-display quadrant, then adds audio and status-light feedback so one channel does not carry the whole accessibility burden.
  • State: menu location, selected item, preference value, and online/status message must be visible or audible.
  • Feedback: navigation, selection, back/cancel, error, success, and toggle events should have distinguishable patterns.
  • Persistence: high contrast, large text, sound, and brightness choices should survive a restart through ESP32 Preferences/NVS.

The lab is successful only if the evidence covers several user situations. A person looking directly at the OLED should see menu position and preference state. A person glancing from a distance should still notice the status LED. A person who cannot read the screen should hear whether a setting was enabled or disabled. A maintainer should be able to inspect serial output and saved preferences after a restart. Those checks turn the exercise from “the code compiles” into an accessibility review.

A practical acceptance test is to hand the simulator to someone who did not write the code, ask them to change one preference, restart the device, and explain the current state using only the cues they noticed. Any hesitation points to a feedback, language, or recovery gap.

UX UmaCheckpoint: Test Evidence

You now know:

  • This lab is not just a wiring exercise; it asks whether a user can navigate, confirm, recover, and preserve preferences.
  • Evidence must cover OLED state, tone pattern, LED blink, serial output, and saved settings.
  • A successful run ends with a design decision, not only a working sketch.

29.5 Measure Interaction Loop

The next step turns that acceptance idea into a repeatable loop. Instead of pressing buttons casually, treat each menu action as a measured run with expected and observed results.

Use Wokwi as a repeatable lab bench. Confirm the I2C wiring for the SSD1306 display on GPIO 21/22, active-low button inputs on GPIO 25/26/27/14 with internal pull-ups, buzzer output on GPIO 13, and status LED on GPIO 2. Then run task scripts such as “enable high contrast,” “increase brightness,” “turn sound off,” and “return to Home without losing position.”

Record what the simulator shows and what the serial monitor reports. Useful evidence includes debounce behavior around the 200 ms window, tone frequency patterns for enable/disable, menu wraparound, OLED readability in normal and large-text modes, brightness dimming, and NVS values after restart. If a user must remember a hidden mode, the interface is doing too much work in the user’s head.

Use a simple run sheet rather than free-form notes. For each task, write the starting menu, button sequence, expected OLED text, expected tone, LED blink pattern, serial event, saved preference key, and observed mismatch. Repeat at least one task with sound muted and one task with large text enabled, because those modes change what counts as adequate feedback. If Wokwi behavior is used as evidence, label which parts still need physical validation: real button bounce, buzzer volume, OLED viewing angle, enclosure placement, and power draw.

  1. Run the task: start from Home, complete one accessibility change, and return to the previous screen.
  2. Check all channels: verify OLED text, LED blink count, buzzer tone, serial log, and saved preference.
  3. Record the gap: note any ambiguous state, missing confirmation, hard-to-read display, excessive delay, or accidental double press.

A good lab record ends with a design decision. Examples include keeping the 200 ms debounce window, shortening a tone that blocks input too long, adding a confirmation label beside a toggle, or moving brightness recovery into the top-level menu. Each decision should include the evidence that justified it and the hardware condition that would require a bench recheck. ## Embedded UI Shapes Accessibility {.depth-l2}

The code decisions in this lab are interface decisions. INPUT_PULLUP changes the button’s electrical behavior and makes pressed read as LOW. The debounce delay changes whether rapid presses feel responsive or create accidental repeats. tone() and blocking delay() calls can make audio feedback clear, but they can also pause input handling if overused. SSD1306 text size and display contrast decide whether the interface is glanceable at 128x64 pixels.

Preference storage is part of the UX contract. ESP32 Preferences writes to non-volatile storage so high contrast, large text, sound, and brightness settings survive power cycles. That improves accessibility only if the saved state is applied before the first meaningful screen and if the user can still recover when a setting makes the interface less usable.

  • Timing: compare debounce, audio duration, display refresh, and human reaction time.
  • Failure: test display-init failure, muted sound, low brightness, repeated button presses, and restart after saved settings.
  • Transfer: before building hardware, identify which simulator assumptions need bench validation: button bounce, buzzer volume, OLED visibility, enclosure placement, and power draw.

The simulator hides some physical risks. Real switches bounce differently from Wokwi buttons, real buzzers have volume and frequency limits, and an OLED that is readable on a monitor may be hard to read behind a plastic window or at an installed angle. The under-the-hood review should therefore keep separate columns for simulated evidence and bench evidence. Wokwi can validate state logic, menu routing, preference persistence, and serial instrumentation, but it cannot close enclosure, acoustics, accessibility reach, or battery-runtime questions.

Those boundaries matter when the lab becomes a product pattern. If a later device uses an interrupt-driven input handler, different display library, haptic motor, or asynchronous audio driver, the interface evidence must be retested. The user promise is not “the sketch ran”; it is that every important state has a clear, recoverable, multimodal cue under realistic timing and failure conditions.

UX UmaCheckpoint: Simulator Boundaries

You now know:

  • Wokwi is strong evidence for menu routing, state logic, preference persistence, and serial instrumentation.
  • Bench validation is still needed for bounce, buzzer volume, OLED viewing angle, enclosure placement, and power draw.
  • The saved accessibility state must be applied before the first meaningful screen, or persistence becomes confusing rather than helpful.

29.6 Lab: Build an Accessible IoT Interface

Now build the interface. Keep the same evidence mindset while you copy the circuit, code, and diagram: every line exists to make a user-visible state clearer through at least one channel.

This hands-on lab uses the Wokwi ESP32 simulator to build an accessible IoT interface with menu navigation, visual feedback on an OLED display, audio feedback via buzzer, and user preference storage.

29.6.1 Components Used

Component Purpose Connection
ESP32 DevKit Main microcontroller Central board
SSD1306 OLED 128x64 Visual display output I2C (GPIO 21 SDA, GPIO 22 SCL)
Push Button (Up) Menu navigation up GPIO 25
Push Button (Down) Menu navigation down GPIO 26
Push Button (Select) Menu selection/confirm GPIO 27
Push Button (Back) Return to previous menu GPIO 14
Passive Buzzer Audio feedback GPIO 13
LED (Status) Visual status indicator GPIO 2 (built-in)

29.6.2 Key UI/UX Concepts in This Lab

Accessibility Design Principles

This lab demonstrates core accessibility concepts:

  1. Multimodal Feedback: Every action produces visual (OLED + LED), audio (buzzer), and timing-based feedback
  2. High Contrast Mode: Toggle between standard and high-contrast display modes for users with visual impairments
  3. Adjustable Text Size: Switch between normal and large text modes for readability
  4. Audio Confirmation: Distinct sound patterns for navigation, selection, errors, and confirmation
  5. Progressive Disclosure: Show only relevant options at each menu level to reduce cognitive load
  6. Persistent Preferences: Save user accessibility settings so they persist across power cycles
  7. Debounced Input: Handle button bouncing to prevent accidental double-presses

29.6.3 Circuit Diagram

Circuit diagram showing ESP32 connected to SSD1306 OLED display via I2C, four navigation buttons on GPIO pins, a passive buzzer for audio feedback, and built-in LED for accessible IoT interface lab
Figure 29.2: Circuit diagram showing ESP32 connected to SSD1306 OLED display via I2C, four navigation buttons on GPIO pins, a passive buzzer for audio feedback, and built-in LED for accessible IoT interface lab

29.6.4 Wokwi Simulator Environment

About Wokwi

Wokwi is a free online simulator for Arduino, ESP32, and other microcontrollers. It allows you to build and test IoT projects entirely in your browser without purchasing hardware. The simulator supports OLED displays, buttons, buzzers, and other components needed for this accessibility-focused UI lab.

Launch the simulator below to get started:

Simulator Tips
  • Click the + button to add components (search for “SSD1306”, “Button”, “Buzzer”)
  • Connect wires by clicking on pins
  • Use internal pull-up resistors for buttons (configured in code)
  • The Serial Monitor shows debug output and accessibility events
  • Press the green Play button to run your code
  • Test both high contrast and standard display modes

29.6.5 Step-by-Step Instructions

29.6.5.1 Step 1: Set Up the Circuit

  1. Add an SSD1306 OLED Display: Click + and search for “SSD1306”
  2. Add 4 Push Buttons: Click + and add 4 “Push Button” components
  3. Add a Passive Buzzer: Click + and search for “Buzzer”
  4. Wire the components to ESP32:

OLED Display (I2C):

  • OLED VCC -> ESP32 3.3V
  • OLED GND -> ESP32 GND
  • OLED SDA -> ESP32 GPIO 21
  • OLED SCL -> ESP32 GPIO 22

Navigation Buttons (active LOW with internal pull-up):

  • UP Button: One leg to GPIO 25, other leg to GND
  • DOWN Button: One leg to GPIO 26, other leg to GND
  • SELECT Button: One leg to GPIO 27, other leg to GND
  • BACK Button: One leg to GPIO 14, other leg to GND

Audio Feedback:

  • Buzzer (+) -> ESP32 GPIO 13
  • Buzzer (-) -> ESP32 GND

29.6.5.2 Step 2: Understanding the Menu Structure

Before copying the code, understand the accessible menu system:

Menu hierarchy diagram showing main menu with Home, Settings, and Status options, where Settings expands to Accessibility submenu containing High Contrast toggle, Large Text toggle, Sound toggle, and Brightness slider options
Figure 29.3: Menu hierarchy diagram showing main menu with Home, Settings, and Status options, where Settings expands to Accessibility submenu containing High Contrast toggle, Large Text toggle, Sound toggle, and Brightness slider options

29.6.5.3 Step 3: Copy the Accessible Interface Code

Copy the following code into the Wokwi code editor (replace any existing code):

Full Accessible Lab Code
/*
 * Accessible IoT Interface Lab
 *
 * Demonstrates core UI/UX accessibility principles:
 * - Multimodal feedback (visual, audio, timing)
 * - High contrast mode for visual impairments
 * - Large text mode for readability
 * - Audio confirmation sounds
 * - Persistent user preferences (NVS)
 * - Debounced button input
 *
 * Compatible with: ESP32 DevKit, Wokwi Simulator
 * Display: SSD1306 OLED 128x64 (I2C)
 */

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>

// ========== DISPLAY CONFIGURATION ==========
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ========== PIN DEFINITIONS ==========
#define BTN_UP 25
#define BTN_DOWN 26
#define BTN_SELECT 27
#define BTN_BACK 14
#define BUZZER_PIN 13
#define LED_PIN 2

// ========== TIMING CONSTANTS ==========
#define DEBOUNCE_DELAY 200
#define MENU_ANIMATION_DELAY 50
#define FEEDBACK_DURATION 100

// ========== AUDIO FEEDBACK FREQUENCIES ==========
#define TONE_NAVIGATE 800    // Navigation beep
#define TONE_SELECT 1200     // Selection confirmation
#define TONE_BACK 400        // Back/cancel tone
#define TONE_ERROR 200       // Error tone
#define TONE_SUCCESS 1600    // Success confirmation
#define TONE_TOGGLE_ON 1000  // Toggle enabled
#define TONE_TOGGLE_OFF 600  // Toggle disabled

// ========== USER PREFERENCES ==========
Preferences preferences;

struct AccessibilitySettings {
  bool highContrast;
  bool largeText;
  bool soundEnabled;
  uint8_t brightness;
} settings;

// ========== MENU SYSTEM ==========
enum MenuState {
  MENU_HOME,
  MENU_MAIN,
  MENU_SETTINGS,
  MENU_ACCESSIBILITY,
  MENU_SYSTEM_INFO
};

MenuState currentMenu = MENU_HOME;
int menuIndex = 0;
int maxMenuItems = 3;

// Menu item labels
const char* mainMenuItems[] = {"Home", "Settings", "System Info"};
const char* accessibilityItems[] = {"High Contrast", "Large Text", "Sound", "Brightness", "Back"};

// ========== DEBOUNCING ==========
unsigned long lastButtonPress = 0;

// ========== FUNCTION DECLARATIONS ==========
void loadSettings();
void saveSettings();
void playTone(int frequency, int duration);
void playNavigateSound();
void playSelectSound();
void playBackSound();
void playToggleSound(bool enabled);
void drawMenu();
void drawHome();
void drawMainMenu();
void drawAccessibilityMenu();
void drawSystemInfo();
void handleNavigation(int direction);
void handleSelect();
void handleBack();
void updateDisplay();
void showFeedback(const char* message, bool success);
void blinkLED(int times);

// ========== SETUP ==========
void setup() {
  Serial.begin(115200);
  Serial.println("\n=== Accessible IoT Interface Lab ===");
  Serial.println("Demonstrating UI/UX accessibility principles");

  // Initialize pins
  pinMode(BTN_UP, INPUT_PULLUP);
  pinMode(BTN_DOWN, INPUT_PULLUP);
  pinMode(BTN_SELECT, INPUT_PULLUP);
  pinMode(BTN_BACK, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);

  // Initialize display
  Wire.begin(21, 22);
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println("ERROR: SSD1306 allocation failed");
    for (;;); // Halt if display fails
  }

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.display();

  // Load saved preferences
  loadSettings();

  // Apply brightness setting
  display.dim(settings.brightness < 50);

  // Welcome feedback
  Serial.println("Settings loaded:");
  Serial.printf("  High Contrast: %s\n", settings.highContrast ? "ON" : "OFF");
  Serial.printf("  Large Text: %s\n", settings.largeText ? "ON" : "OFF");
  Serial.printf("  Sound: %s\n", settings.soundEnabled ? "ON" : "OFF");
  Serial.printf("  Brightness: %d%%\n", settings.brightness);

  // Startup sound and visual
  if (settings.soundEnabled) {
    playTone(TONE_SUCCESS, 100);
    delay(50);
    playTone(TONE_SUCCESS + 200, 100);
  }
  blinkLED(2);

  // Show home screen
  drawHome();
}

// ========== MAIN LOOP ==========
void loop() {
  // Check for button presses with debouncing
  if (millis() - lastButtonPress > DEBOUNCE_DELAY) {

    // UP button
    if (digitalRead(BTN_UP) == LOW) {
      lastButtonPress = millis();
      handleNavigation(-1);
      Serial.println("BTN: UP pressed");
    }

    // DOWN button
    else if (digitalRead(BTN_DOWN) == LOW) {
      lastButtonPress = millis();
      handleNavigation(1);
      Serial.println("BTN: DOWN pressed");
    }

    // SELECT button
    else if (digitalRead(BTN_SELECT) == LOW) {
      lastButtonPress = millis();
      handleSelect();
      Serial.println("BTN: SELECT pressed");
    }

    // BACK button
    else if (digitalRead(BTN_BACK) == LOW) {
      lastButtonPress = millis();
      handleBack();
      Serial.println("BTN: BACK pressed");
    }
  }

  delay(10); // Small delay for stability
}

// ========== SETTINGS MANAGEMENT ==========
void loadSettings() {
  preferences.begin("access", false);
  settings.highContrast = preferences.getBool("contrast", false);
  settings.largeText = preferences.getBool("largetext", false);
  settings.soundEnabled = preferences.getBool("sound", true);
  settings.brightness = preferences.getUChar("brightness", 100);
  preferences.end();
}

void saveSettings() {
  preferences.begin("access", false);
  preferences.putBool("contrast", settings.highContrast);
  preferences.putBool("largetext", settings.largeText);
  preferences.putBool("sound", settings.soundEnabled);
  preferences.putUChar("brightness", settings.brightness);
  preferences.end();
  Serial.println("Settings saved to NVS");
}

// ========== AUDIO FEEDBACK ==========
void playTone(int frequency, int duration) {
  if (!settings.soundEnabled) return;
  tone(BUZZER_PIN, frequency, duration);
  delay(duration);
  noTone(BUZZER_PIN);
}

void playNavigateSound() {
  playTone(TONE_NAVIGATE, 30);
}

void playSelectSound() {
  playTone(TONE_SELECT, 50);
  delay(30);
  playTone(TONE_SELECT + 200, 50);
}

void playBackSound() {
  playTone(TONE_BACK, 80);
}

void playToggleSound(bool enabled) {
  if (enabled) {
    playTone(TONE_TOGGLE_ON, 50);
    delay(30);
    playTone(TONE_TOGGLE_ON + 400, 80);
  } else {
    playTone(TONE_TOGGLE_OFF + 200, 50);
    delay(30);
    playTone(TONE_TOGGLE_OFF, 80);
  }
}

void playErrorSound() {
  playTone(TONE_ERROR, 100);
  delay(50);
  playTone(TONE_ERROR, 100);
}

// ========== VISUAL FEEDBACK ==========
void blinkLED(int times) {
  for (int i = 0; i < times; i++) {
    digitalWrite(LED_PIN, HIGH);
    delay(100);
    digitalWrite(LED_PIN, LOW);
    delay(100);
  }
}

void showFeedback(const char* message, bool success) {
  // Visual feedback on display
  display.fillRect(0, 54, 128, 10, settings.highContrast ? SSD1306_WHITE : SSD1306_BLACK);
  display.setTextColor(settings.highContrast ? SSD1306_BLACK : SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(4, 55);
  display.print(message);
  display.display();

  // Audio feedback
  if (success) {
    playTone(TONE_SUCCESS, 100);
  } else {
    playErrorSound();
  }

  // LED feedback
  blinkLED(success ? 1 : 3);

  delay(500);
  updateDisplay();
}

// ========== NAVIGATION HANDLING ==========
void handleNavigation(int direction) {
  menuIndex += direction;

  // Wrap around menu
  if (menuIndex < 0) menuIndex = maxMenuItems - 1;
  if (menuIndex >= maxMenuItems) menuIndex = 0;

  playNavigateSound();
  blinkLED(1);
  updateDisplay();
}

void handleSelect() {
  playSelectSound();
  blinkLED(1);

  switch (currentMenu) {
    case MENU_HOME:
      currentMenu = MENU_MAIN;
      menuIndex = 0;
      maxMenuItems = 3;
      break;

    case MENU_MAIN:
      switch (menuIndex) {
        case 0: // Home
          currentMenu = MENU_HOME;
          break;
        case 1: // Settings -> Accessibility
          currentMenu = MENU_ACCESSIBILITY;
          menuIndex = 0;
          maxMenuItems = 5;
          break;
        case 2: // System Info
          currentMenu = MENU_SYSTEM_INFO;
          break;
      }
      break;

    case MENU_ACCESSIBILITY:
      switch (menuIndex) {
        case 0: // Toggle High Contrast
          settings.highContrast = !settings.highContrast;
          playToggleSound(settings.highContrast);
          saveSettings();
          Serial.printf("High Contrast: %s\n", settings.highContrast ? "ON" : "OFF");
          break;
        case 1: // Toggle Large Text
          settings.largeText = !settings.largeText;
          playToggleSound(settings.largeText);
          saveSettings();
          Serial.printf("Large Text: %s\n", settings.largeText ? "ON" : "OFF");
          break;
        case 2: // Toggle Sound
          settings.soundEnabled = !settings.soundEnabled;
          // Play sound BEFORE disabling
          if (!settings.soundEnabled) {
            tone(BUZZER_PIN, TONE_TOGGLE_OFF + 200, 50);
            delay(30);
            tone(BUZZER_PIN, TONE_TOGGLE_OFF, 80);
            noTone(BUZZER_PIN);
          } else {
            playToggleSound(true);
          }
          saveSettings();
          Serial.printf("Sound: %s\n", settings.soundEnabled ? "ON" : "OFF");
          break;
        case 3: // Adjust Brightness
          settings.brightness += 25;
          if (settings.brightness > 100) settings.brightness = 25;
          display.dim(settings.brightness < 50);
          playTone(800 + (settings.brightness * 8), 50);
          saveSettings();
          Serial.printf("Brightness: %d%%\n", settings.brightness);
          break;
        case 4: // Back
          handleBack();
          return;
      }
      break;

    case MENU_SYSTEM_INFO:
      // Return to main menu
      handleBack();
      return;
  }

  updateDisplay();
}

void handleBack() {
  playBackSound();
  blinkLED(1);

  switch (currentMenu) {
    case MENU_HOME:
      // Already at home, do nothing
      break;
    case MENU_MAIN:
      currentMenu = MENU_HOME;
      break;
    case MENU_ACCESSIBILITY:
    case MENU_SYSTEM_INFO:
      currentMenu = MENU_MAIN;
      menuIndex = 0;
      maxMenuItems = 3;
      break;
  }

  updateDisplay();
}

// ========== DISPLAY RENDERING ==========
void updateDisplay() {
  switch (currentMenu) {
    case MENU_HOME:
      drawHome();
      break;
    case MENU_MAIN:
      drawMainMenu();
      break;
    case MENU_ACCESSIBILITY:
      drawAccessibilityMenu();
      break;
    case MENU_SYSTEM_INFO:
      drawSystemInfo();
      break;
  }
}

void drawHome() {
  display.clearDisplay();

  // Background for high contrast mode
  if (settings.highContrast) {
    display.fillScreen(SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
  } else {
    display.setTextColor(SSD1306_WHITE);
  }

  // Title
  display.setTextSize(settings.largeText ? 2 : 1);
  display.setCursor(settings.largeText ? 10 : 25, 4);
  display.print("IoT Device");

  // Status indicator with icon
  display.setTextSize(1);
  display.setCursor(4, settings.largeText ? 28 : 20);
  display.print("Status: ");
  display.print("ONLINE");

  // Draw status indicator circle
  int circleX = settings.largeText ? 100 : 75;
  int circleY = settings.largeText ? 32 : 23;
  if (settings.highContrast) {
    display.fillCircle(circleX, circleY, 4, SSD1306_BLACK);
  } else {
    display.fillCircle(circleX, circleY, 4, SSD1306_WHITE);
  }

  // Show active accessibility features
  display.setCursor(4, settings.largeText ? 44 : 36);
  if (settings.highContrast || settings.largeText) {
    display.print("A11y: ");
    if (settings.highContrast) display.print("HC ");
    if (settings.largeText) display.print("LT ");
  }

  // Navigation hint
  display.setCursor(4, 54);
  display.setTextSize(1);
  display.print("[SELECT] Open Menu");

  display.display();
}

void drawMainMenu() {
  display.clearDisplay();

  if (settings.highContrast) {
    display.fillScreen(SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
  } else {
    display.setTextColor(SSD1306_WHITE);
  }

  // Title
  display.setTextSize(settings.largeText ? 2 : 1);
  display.setCursor(settings.largeText ? 20 : 40, 2);
  display.print("MENU");

  // Draw menu items
  display.setTextSize(settings.largeText ? 2 : 1);
  int yStart = settings.largeText ? 22 : 18;
  int ySpacing = settings.largeText ? 14 : 12;

  for (int i = 0; i < 3; i++) {
    int y = yStart + (i * ySpacing);

    // Highlight selected item
    if (i == menuIndex) {
      if (settings.highContrast) {
        display.fillRect(0, y - 2, 128, ySpacing, SSD1306_BLACK);
        display.setTextColor(SSD1306_WHITE);
      } else {
        display.fillRect(0, y - 2, 128, ySpacing, SSD1306_WHITE);
        display.setTextColor(SSD1306_BLACK);
      }
      display.setCursor(4, y);
      display.print("> ");
    } else {
      display.setTextColor(settings.highContrast ? SSD1306_BLACK : SSD1306_WHITE);
      display.setCursor(4, y);
      display.print("  ");
    }
    display.print(mainMenuItems[i]);
  }

  // Navigation hints
  display.setTextSize(1);
  display.setTextColor(settings.highContrast ? SSD1306_BLACK : SSD1306_WHITE);
  display.setCursor(4, 54);
  display.print("[UP/DN] Nav [SEL] OK");

  display.display();
}

void drawAccessibilityMenu() {
  display.clearDisplay();

  if (settings.highContrast) {
    display.fillScreen(SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
  } else {
    display.setTextColor(SSD1306_WHITE);
  }

  // Title
  display.setTextSize(1);
  display.setCursor(20, 2);
  display.print("ACCESSIBILITY");

  // Draw menu items with current values
  int yStart = 14;
  int ySpacing = 10;

  for (int i = 0; i < 5; i++) {
    int y = yStart + (i * ySpacing);

    // Highlight selected item
    if (i == menuIndex) {
      if (settings.highContrast) {
        display.fillRect(0, y - 1, 128, ySpacing, SSD1306_BLACK);
        display.setTextColor(SSD1306_WHITE);
      } else {
        display.fillRect(0, y - 1, 128, ySpacing, SSD1306_WHITE);
        display.setTextColor(SSD1306_BLACK);
      }
    } else {
      display.setTextColor(settings.highContrast ? SSD1306_BLACK : SSD1306_WHITE);
    }

    display.setCursor(4, y);
    if (i == menuIndex) display.print(">");
    display.setCursor(12, y);
    display.print(accessibilityItems[i]);

    // Show current values
    display.setCursor(90, y);
    switch (i) {
      case 0: display.print(settings.highContrast ? "[ON]" : "[OFF]"); break;
      case 1: display.print(settings.largeText ? "[ON]" : "[OFF]"); break;
      case 2: display.print(settings.soundEnabled ? "[ON]" : "[OFF]"); break;
      case 3:
        display.print("[");
        display.print(settings.brightness);
        display.print("%]");
        break;
      case 4: display.print(""); break;
    }
  }

  // Navigation hints
  display.setTextColor(settings.highContrast ? SSD1306_BLACK : SSD1306_WHITE);
  display.setCursor(4, 54);
  display.print("[SEL] Toggle [BACK]");

  display.display();
}

void drawSystemInfo() {
  display.clearDisplay();

  if (settings.highContrast) {
    display.fillScreen(SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
  } else {
    display.setTextColor(SSD1306_WHITE);
  }

  // Title
  display.setTextSize(1);
  display.setCursor(25, 2);
  display.print("SYSTEM INFO");

  // System information
  display.setCursor(4, 16);
  display.print("Device: ESP32");

  display.setCursor(4, 26);
  display.print("Uptime: ");
  display.print(millis() / 1000);
  display.print("s");

  display.setCursor(4, 36);
  display.print("Free Heap: ");
  display.print(ESP.getFreeHeap() / 1024);
  display.print("KB");

  display.setCursor(4, 46);
  display.print("Display: 128x64 OLED");

  // Navigation hint
  display.setCursor(4, 54);
  display.print("[BACK] Return");

  display.display();
}

29.6.5.4 Step 4: Create the diagram.json File

In Wokwi, click on the diagram.json tab and replace its contents with:

{
  "version": 1,
  "author": "IoT Class",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-esp32-devkit-v1", "id": "esp", "top": 0, "left": 0, "attrs": {} },
    {
      "type": "wokwi-ssd1306",
      "id": "oled1",
      "top": -100,
      "left": 150,
      "attrs": { "i2cAddress": "0x3c" }
    },
    { "type": "wokwi-pushbutton", "id": "btn1", "top": 100, "left": 200, "attrs": { "color": "green", "label": "UP" } },
    { "type": "wokwi-pushbutton", "id": "btn2", "top": 140, "left": 200, "attrs": { "color": "blue", "label": "DOWN" } },
    { "type": "wokwi-pushbutton", "id": "btn3", "top": 180, "left": 200, "attrs": { "color": "red", "label": "SELECT" } },
    { "type": "wokwi-pushbutton", "id": "btn4", "top": 220, "left": 200, "attrs": { "color": "yellow", "label": "BACK" } },
    { "type": "wokwi-buzzer", "id": "bz1", "top": 100, "left": -80, "attrs": {} }
  ],
  "connections": [
    [ "esp:TX0", "$serialMonitor:RX", "", [] ],
    [ "esp:RX0", "$serialMonitor:TX", "", [] ],
    [ "oled1:GND", "esp:GND.1", "black", [ "v20", "h-60" ] ],
    [ "oled1:VCC", "esp:3V3", "red", [ "v30", "h-80" ] ],
    [ "oled1:SDA", "esp:21", "green", [ "v40", "h-100" ] ],
    [ "oled1:SCL", "esp:22", "blue", [ "v50", "h-120" ] ],
    [ "btn1:1.l", "esp:25", "green", [ "h-50" ] ],
    [ "btn1:2.l", "esp:GND.1", "black", [ "h-30", "v50" ] ],
    [ "btn2:1.l", "esp:26", "blue", [ "h-60" ] ],
    [ "btn2:2.l", "esp:GND.1", "black", [ "h-40", "v30" ] ],
    [ "btn3:1.l", "esp:27", "red", [ "h-70" ] ],
    [ "btn3:2.l", "esp:GND.1", "black", [ "h-50", "v10" ] ],
    [ "btn4:1.l", "esp:14", "yellow", [ "h-80" ] ],
    [ "btn4:2.l", "esp:GND.1", "black", [ "h-60", "v-10" ] ],
    [ "bz1:1", "esp:13", "orange", [ "h30" ] ],
    [ "bz1:2", "esp:GND.1", "black", [ "h20", "v30" ] ]
  ]
}

29.6.5.5 Step 5: Run and Test

  1. Press the green Play button to start the simulation
  2. Observe the home screen showing device status
  3. Press SELECT to open the main menu
  4. Use UP/DOWN to navigate between options
  5. Press SELECT on “Settings” to access accessibility options
  6. Toggle High Contrast - notice the inverted display colors
  7. Toggle Large Text - see the text size increase
  8. Toggle Sound - hear different tones for each action
  9. Adjust Brightness - cycle through brightness levels
  10. Press BACK to return to previous menus

29.6.6 Learning Points

After the simulator runs, step back from the mechanics and read the result as an interaction pattern. The important question is which cue carries which state, and what happens when one cue is unavailable.

Key UI/UX Concepts Demonstrated

Multimodal Feedback Design:

Action Visual Audio Timing
Navigation Menu highlight moves 800Hz beep LED blink
Selection Screen updates Rising tone LED blink
Toggle ON Value shows [ON] Ascending tones LED blink
Toggle OFF Value shows [OFF] Descending tones LED blink
Back Previous screen 400Hz tone LED blink
Error Error message Double low beep Triple LED blink

Accessibility Features Implemented:

  1. High Contrast Mode: Inverts display colors for users with low vision. White background with black text provides maximum contrast ratio (~21:1).

  2. Large Text Mode: Increases text size from 1x to 2x for improved readability.

  3. Audio Feedback: Distinct tones for different actions help users who cannot see the display clearly. Ascending tones indicate positive actions, descending tones indicate going back or disabling features.

  4. Persistent Preferences: Settings are saved to NVS (Non-Volatile Storage) and automatically restored on power-up.

  5. Button-Based Navigation: Physical buttons work for users who cannot use touchscreens. Internal pull-up resistors simplify wiring.

  6. Debouncing: 200ms debounce delay prevents accidental double-presses, important for users with motor control difficulties.

Design Patterns Used:

  • Progressive Disclosure: Only relevant options shown at each menu level
  • Consistent Navigation: Same buttons always perform same functions
  • Status Indicators: Visual feedback shows current state at all times
  • Error Prevention: Menu wrapping prevents “out of bounds” navigation

29.6.7 Challenge Exercises

Extend Your Learning

Try these modifications to deepen your understanding of accessible interface design:

Challenge 1: Add Haptic Feedback Patterns Modify the buzzer code to create distinct vibration-like patterns (rapid tone sequences) that convey different meanings.

Challenge 2: Implement Voice-Over Simulation Add Serial output that “announces” the current menu item and its state when navigation occurs. This simulates screen reader functionality.

Challenge 3: Add Auto-Repeat for Held Buttons Implement a feature where holding UP or DOWN buttons automatically scrolls through menu items at increasing speed.

Challenge 4: Create Color-Coded Status LEDs Add RGB LED support to show different status colors (green = normal, yellow = settings, blue = system info, red = error).

Challenge 5: Implement Timeout and Screen Saver Add a screen timeout that dims or blanks the display after 30 seconds of inactivity.

Challenge 6: Add Language Selection Implement a simple language toggle between English and Spanish to demonstrate internationalization accessibility.

29.6.8 Knowledge Check

Test Your Understanding
  1. Why does the code use internal pull-up resistors for buttons?
    • Simplifies wiring (no external resistors needed)
    • Buttons read LOW when pressed, HIGH when released
    • More reliable than external pull-downs in noisy environments
  2. What accessibility standard does high contrast mode support?
    • WCAG 2.1 Level AA requires 4.5:1 contrast ratio for normal text
    • The inverted OLED display achieves approximately 21:1 contrast ratio
  3. Why are different audio tones used for different actions?
    • Users can distinguish actions by sound alone (without seeing display)
    • Ascending tones = positive/forward actions
    • Descending tones = negative/backward actions
  4. What is the purpose of the NVS (Non-Volatile Storage) in this design?
    • Persists user preferences across power cycles
    • Users do not need to reconfigure accessibility settings each boot

29.6.9 Real-World Applications

This accessible interface pattern applies to many IoT devices:

Device Type Accessibility Need Implementation
Smart Thermostat Vision impaired users High contrast display, audio temperature announcements
Medical Alert Device Elderly users Large buttons, loud audio feedback, simple menus
Industrial HMI Noisy environments Visual + haptic feedback since audio may be inaudible
Smart Home Hub Multiple family members User profiles with personal accessibility settings
Wearable Devices On-the-go interaction Glanceable display, haptic confirmation
Accessibility Testing Issues

A team built this accessible interface lab project and tested it with 6 users: 3 with typical vision/hearing, 2 with low vision, 1 with hearing impairment.

Test Results:

Participant Disability Finding Fix Applied
P1 (typical) None Navigated perfectly in 12s Baseline established
P2 (typical) None Preferred large text mode even with good vision—“easier to read from across desk” Validated feature useful beyond target users
P3 (low vision) Mild macular degeneration High contrast mode worked well BUT complained menu text still too small in standard size Increased large text from 2× to 2.5× scale
P4 (low vision) Severe cataracts Could not distinguish high-contrast mode LED status indicator (too small) Added audio announcement: “High contrast enabled”
P5 (hearing) Moderate hearing loss Could not hear buzzer tones below 800Hz Shifted all tones to 1000-1600Hz range (higher pitched)
P6 (typical) None Successfully used device but asked “Why no voice control?” Revealed assumption: accessibility = visual/audio only. Added voice control to roadmap

Key Insights:

  1. High contrast helps everyone: 4/6 users preferred high contrast mode even with typical vision—“less eye strain”
  2. Audio frequency matters: Original 200-800Hz tones perfect for young users, inaudible to 40+ age group
  3. Accessibility cascades: Features designed for disabilities improve experience for all (larger text = readable from distance; audio confirmation = works when not looking)

Cost of fixing pre-deployment: $240 (8 hours × $30/hr developer time) Cost if discovered post-deployment: $15,000 (firmware updates to 5,000 units, support tickets, reputation damage)

Revised success rate: Checked again with the same 6 users after fixes: 100% task completion, 9.2/10 average satisfaction (up from 7.1/10).

UX UmaCheckpoint: Accessibility Tradeoffs

You now know:

  • High contrast and larger text help low-vision users, but they also improve glanceable use for many people.
  • Audio feedback must be designed around actual hearing, frequency, and environment rather than assumed from a desk test.
  • A small pre-deployment fix can prevent expensive firmware, support, and reputation problems after launch.
Add Accessibility Features
Feature Development Cost User Impact Priority
High contrast mode (invert colors) 2 hours 15-20% of users benefit (low vision, bright sunlight) HIGH — Easy to implement, high impact
Large text mode (2× scale) 3 hours 10-15% need it, 40% prefer it HIGH — Trivial cost, broad benefit
Audio confirmation (beeps/tones) 4 hours Essential for 5% (vision impaired), useful for 60% (multitasking) HIGH — Multimodal feedback core principle
Voice control 40-60 hours 25% use regularly, 60% use occasionally MEDIUM — High value but significant development time
Haptic feedback (vibration) 8 hours (hardware) 15% benefit (hearing impaired, noisy environments) MEDIUM — Requires hardware component ($2-5/unit)
Screen reader compatibility 20-30 hours 2-3% of users LOW-MEDIUM — Critical for blind users but small %, required for gov contracts

Decision rule:

  • HIGH priority: Implement in v1.0 — low cost, high impact, improves experience for all users
  • MEDIUM priority: Implement in v1.1-1.2 — worthwhile but requires more development time
  • LOW priority: Implement if legally required (ADA compliance) or if targeting specific accessibility market

ROI Example:

  • Adding high contrast + large text costs ~5 hours ($150 developer time)
  • Reduces support calls from vision-impaired users by 80% (saves $3,000/year for 1,000-unit deployment)
  • Improves app store ratings from 4.1★ to 4.6★ (10% sales increase = $50,000 for typical IoT product)

The priority table helps decide what to build first. The next warning explains why that decision must happen early, before the interface architecture hardens around a single mode such as touch.

Accessibility Afterthoughts

The mistake: Building the full product, then trying to “add accessibility” at the end. Result: bolted-on features that don’t integrate well and incomplete coverage.

Real example: A smart home control panel launched with touchscreen-only interface. After disability advocacy complaints, they added: - Voice control (6 months post-launch, only covered 40% of features) - Screen reader support (8 months post-launch, broken on 30% of screens due to missing alt tags) - High contrast mode (never delivered—would require redesigning entire UI)

Why it fails:

  • Design debt: Touchscreen-first design has UI patterns incompatible with voice/screen readers
  • Partial implementation: Adding voice to existing screens one-by-one takes years; features launch without it
  • Expensive retrofitting: Redesigning screens for screen readers costs 3-5× more than building them accessible from day one

The fix: Design for accessibility from day 1

Week 1 (Requirements):

Week 2-4 (Design):

Week 5-8 (Development):

Week 9-10 (Testing):

Cost comparison:

  • Accessible from day 1: +15-20% development time ($15,000 for $100,000 project)
  • Retrofit after launch: 3-5× cost ($45,000-75,000) + reputation damage + potential lawsuits

Remember: Accessibility isn’t a feature—it’s a design constraint. You don’t “add accessibility.” You build accessible products from the start.

Interactive Quiz: Match Concepts

Interactive Quiz: Sequence the Steps

Common Pitfalls

Overbuilt Initial Prototypes

Adding too many features before validating core user needs wastes weeks of effort on a direction that user testing reveals is wrong. IoT projects frequently discover that users want simpler interactions than engineers assumed. Define and test a minimum viable version first, then add complexity only in response to validated user requirements.

Security During Development

Treating security as a phase-2 concern results in architectures (hardcoded credentials, unencrypted channels, no firmware signing) that are expensive to remediate after deployment. Include security requirements in the initial design review, even for prototypes, because prototype patterns become production patterns.

Failure Modes and Recovery

Designing only for the happy path leaves a system that cannot recover gracefully from sensor failures, connectivity outages, or cloud unavailability. Explicitly design and test the behaviour for each failure mode and ensure devices fall back to a safe, locally functional state during outages.

Label the Diagram

💻 Code Challenge

29.7 Summary

This hands-on lab demonstrated accessible IoT interface design:

Key Takeaways:

  1. Multimodal Feedback: Every action confirmed through visual, audio, and timing feedback
  2. High Contrast: Inverted display achieves 21:1 contrast ratio for low vision users
  3. Persistent Preferences: NVS storage ensures settings survive power cycles
  4. Button Navigation: Physical controls with debouncing for motor-impaired users
  5. Progressive Disclosure: Only relevant options shown at each menu level
UX UmaCheckpoint: Lab Handoff

You now know:

  • The lab has a working menu model, a repeatable simulator setup, and a way to compare visual, audio, and timing feedback.
  • The design record should preserve what passed, what failed, and which physical risks still need bench testing.
  • The next useful iteration is an extension or audit that changes one accessibility variable and records the evidence.
For Kids: Meet the Sensor Squad!

Accessibility means making sure EVERYONE can use your device – no matter what!

29.7.1 Build for Everyone

The Sensor Squad built the coolest smart thermostat ever! It had a tiny beautiful screen, whisper-quiet beeps, and sleek tiny buttons. They were SO proud!

Then Grandma Rose came to visit. She squinted at the screen. “The text is so small I can’t read it!” she said.

Uncle Jorge tried next. He was color-blind. “Is the light green or red? I can’t tell!” he said.

Cousin Maria had a cast on her arm. “These buttons are so small I keep pressing the wrong ones!” she said.

The Sensor Squad had a big realization: “We built this for US, not for EVERYONE!”

So they got to work making their thermostat accessible:

  • For Grandma Rose: Sammy added a “LARGE TEXT” mode that made everything big and bold, plus a “HIGH CONTRAST” mode with extra-bright display
  • For Uncle Jorge: Lila added different SOUNDS for different states – a happy rising tone for “heating up” and a gentle falling tone for “cooling down” – so you didn’t need to see colors
  • For Cousin Maria: Max replaced the tiny buttons with BIG buttons that were easy to press, even with a cast
  • For everyone: Bella made sure the device REMEMBERED your preferences, so you didn’t have to set them up every time

“Designing for accessibility actually made the thermostat better for EVERYONE!” realized Max. “Big text is easier to read from across the room, sounds help when you’re not looking, and big buttons are just more satisfying to press!”

29.7.2 Key Words for Kids

Word What It Means
Accessibility Making sure people of ALL abilities can use your device
High Contrast Making text and pictures stand out clearly (like white chalk on a dark board)
Multimodal Feedback Giving information in multiple ways – lights AND sounds AND vibrations
Persistent Settings The device remembers your preferences even when turned off and on again

29.8 Concept Relationships

This hands-on lab integrates accessibility concepts from multiple sources:

Multimodal Feedback = Inclusive Design:

  • Visual (OLED display, LED blinks) for sighted users
  • Audio (buzzer tones) for vision-impaired users or when not looking at screen
  • Timing (debouncing, persistent settings) for motor-impaired users with tremors
  • Together = accessible to widest range of abilities

Accessibility Features Map to Standards:

  • High Contrast Mode (21:1 contrast ratio) meets WCAG 2.1 Level AAA (requires 7:1 for normal text)
  • Large Text Mode (2× scale) addresses low vision users
  • Button Navigation (44px equivalent touch targets) meets WCAG 2.1 Level AA minimum
  • Audio Confirmation provides non-visual feedback (required for screen reader compatibility)
  • Persistent Preferences (NVS storage) prevents user frustration from re-configuring after power cycles

Code Architecture Patterns:

  • State Machine (MenuState enum) organizes complex navigation logic
  • Preference Management (Preferences library) demonstrates IoT data persistence
  • Debouncing (200ms delay) solves real-world hardware button bounce issues
  • Audio Feedback Strategy (distinct frequencies for different actions) creates learnable interaction patterns

Real-World Applications:

  • Smart Thermostats use similar menu navigation + multimodal feedback
  • Medical Devices require accessibility features by law (FDA guidelines)
  • Industrial HMI use OLED displays + physical buttons for reliability in harsh environments

29.9 See Also

Accessibility Design:

Related Labs and Tools:

  • Wokwi ESP32 Simulator - Where this lab runs (no hardware needed)
  • Other IoT Labs - Check /hubs/simulations.html for hardware simulation tools
  • Worked Examples - Voice interface accessibility case study

Standards and Guidelines:

  • WCAG 2.1 Accessibility Guidelines - Web accessibility standards (apply to IoT interfaces too)
  • Section 508 (US Federal) - Accessibility requirements for government IoT systems
  • EN 301 549 (EU) - European accessibility standard for ICT products

Hardware Components:

  • ESP32 datasheet - Microcontroller specifications
  • SSD1306 OLED - Display module used in lab
  • I2C protocol - Communication method between ESP32 and OLED

Code Libraries:

  • Adafruit_SSD1306 - OLED display library
  • Adafruit_GFX - Graphics primitives for displays
  • Preferences (ESP32) - Non-volatile storage library

29.10 Try It Yourself

Extend the accessible interface lab with these challenges:

Challenge 1: Add Voice-Over Simulation (60 minutes)

Implement “screen reader” functionality via Serial output:

void announceMenuItem(int index) {
  Serial.print("Selected: ");
  Serial.println(accessibilityItems[index]);

  // Announce current state for toggles
  switch (index) {
    case 0:
      Serial.printf("High Contrast is currently %s\n",
                    settings.highContrast ? "ON" : "OFF");
      break;
    case 1:
      Serial.printf("Large Text is currently %s\n",
                    settings.largeText ? "ON" : "OFF");
      break;
  }
}

Test: Can someone navigate the interface with eyes closed using only audio announcements?

Challenge 2: Implement Auto-Repeat Navigation (45 minutes)

When user holds UP or DOWN button, automatically scroll through menu:

unsigned long buttonHoldStart = 0;
#define HOLD_THRESHOLD 500  // ms before auto-repeat starts
#define REPEAT_RATE 150     // ms between auto-repeat steps

void loop() {
  if (digitalRead(BTN_UP) == LOW) {
    if (buttonHoldStart == 0) {
      buttonHoldStart = millis();
      handleNavigation(-1);  // First press
    } else if (millis() - buttonHoldStart > HOLD_THRESHOLD) {
      // Auto-repeat mode
      if ((millis() - lastButtonPress) > REPEAT_RATE) {
        handleNavigation(-1);
        lastButtonPress = millis();
      }
    }
  } else {
    buttonHoldStart = 0;  // Reset on release
  }
}

Benefit: Faster navigation for power users, better accessibility for users who struggle with rapid button presses

Challenge 3: Add RGB LED Status Indicators (60 minutes)

Replace single LED with RGB LED showing colored status:

  • Green: Normal operation
  • Yellow: Warning (low brightness, sound disabled)
  • Red: Error (settings failed to save)
  • Blue: Menu navigation active

Accessibility consideration: Don’t rely solely on color - combine with blink patterns

Challenge 4: Implement Gesture Recognition (90 minutes)

Add accelerometer (MPU6050) for gesture control:

  • Shake: Return to home screen
  • Tilt left: Navigate up
  • Tilt right: Navigate down
  • Tap: Select

Test with motor-impaired users: Is gesture control easier than buttons for some users?

Challenge 5: Create Custom Sound Profiles (60 minutes)

Allow users to select sound themes:

enum SoundProfile { CLASSIC, MELODIC, MINIMAL };
SoundProfile soundProfile = CLASSIC;

void playNavigateSound() {
  switch (soundProfile) {
    case CLASSIC:
      playTone(800, 30);  // Original
      break;
    case MELODIC:
      playTone(523, 20);  // C note
      playTone(659, 20);  // E note
      break;
    case MINIMAL:
      playTone(1000, 10);  // Short beep
      break;
  }
}

Benefit: Users with hearing sensitivity can choose less intense sounds; musical users might prefer melodic feedback

Challenge 6: Accessibility Compliance Audit (45 minutes)

Test your interface against WCAG 2.1 AA criteria:

Run it: Instead of scoring the WCAG table from guesswork, audit the design in the Interface Accessibility Checker. Pick the Smart home scenario to match this lab’s device, then move the Text contrast, Text size, Control target, and Alert modalities sliders to your interface’s real values and press Play to watch each check pass or fail. Switch the Operating condition to Gloves + glare and press Stress case to see how contrast and target size hold up away from an ideal bench, then fill the Pass/Fail column below from what the checker reports.

Criterion Requirement Your Implementation Pass/Fail
1.4.3 Contrast 4.5:1 minimum High contrast mode = 21:1 ✓ PASS
1.4.4 Text Resize 200% without loss of functionality Large text mode = 200% ✓ PASS
2.1.1 Keyboard All functionality via keyboard All features accessible via buttons ✓ PASS
2.4.7 Focus Visible Current selection visible Menu highlight + “> ✓ PASS
3.2.1 On Focus No context change on focus Navigation doesn’t trigger actions ✓ PASS

Goal: Achieve 100% WCAG 2.1 AA compliance

Where to Test:

  • Wokwi - Modify the existing project
  • Real ESP32 hardware - Deploy to physical device for tactile testing
  • Interface Design Series - Theory behind these patterns

29.11 What’s Next

Next Topic Description
Interface Design Overview Overview of all chapters in the interface design series
Interface Fundamentals Core UI patterns and component hierarchy
Multimodal Design Accessibility principles for voice, touch, and gesture
Worked Examples Voice interface case study for elderly users
Process & Checklists Design validation checklists and methodology