1511  Interface Design: Hands-On Lab

1511.1 Learning Objectives

By completing this lab, you will be able to:

  • Design Accessible Menus: Create hierarchical navigation that works for users with varying abilities
  • Implement Multimodal Feedback: Provide visual, audio, and tactile confirmation of user actions
  • Apply High Contrast Design: Use color and contrast patterns that meet accessibility standards
  • Store User Preferences: Persist accessibility settings using non-volatile storage (NVS)
  • Handle Diverse Input Methods: Support button navigation for users who cannot use touchscreens
  • Create Glanceable Displays: Design interfaces readable at a glance with clear visual hierarchy

1511.2 Prerequisites

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

1511.3 Lab: Build an Accessible IoT Interface

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.

1511.3.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)

1511.3.2 Key UI/UX Concepts in This Lab

NoteAccessibility 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

1511.3.3 Circuit Diagram

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D'}}}%%
flowchart TB
    subgraph ESP32["ESP32 DevKit"]
        V33["3.3V"]
        G21["GPIO 21 (SDA)"]
        G22["GPIO 22 (SCL)"]
        G25["GPIO 25 (Up)"]
        G26["GPIO 26 (Down)"]
        G27["GPIO 27 (Select)"]
        G14["GPIO 14 (Back)"]
        G13["GPIO 13 (Buzzer)"]
        G2["GPIO 2 (LED)"]
        GND["GND"]
    end

    subgraph OLED["SSD1306 OLED 128x64"]
        VCC["VCC"]
        OGND["GND"]
        SDA["SDA"]
        SCL["SCL"]
    end

    subgraph BUTTONS["Navigation Buttons"]
        BUP["UP Button"]
        BDOWN["DOWN Button"]
        BSEL["SELECT Button"]
        BBACK["BACK Button"]
    end

    subgraph AUDIO["Audio Feedback"]
        BUZ["Passive Buzzer"]
    end

    V33 -.->|"Power"| VCC
    G21 -.->|"I2C Data"| SDA
    G22 -.->|"I2C Clock"| SCL
    GND -.->|"Ground"| OGND

    G25 -.->|"Pull-up"| BUP
    G26 -.->|"Pull-up"| BDOWN
    G27 -.->|"Pull-up"| BSEL
    G14 -.->|"Pull-up"| BBACK

    G13 -.->|"PWM Audio"| BUZ

    style ESP32 fill:#2C3E50,stroke:#16A085,stroke-width:2px,color:#fff
    style OLED fill:#16A085,stroke:#2C3E50,stroke-width:2px,color:#fff
    style BUTTONS fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff
    style AUDIO fill:#7F8C8D,stroke:#2C3E50,stroke-width:2px,color:#fff

Figure 1511.1: 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

1511.3.4 Wokwi Simulator Environment

NoteAbout 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:

TipSimulator 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

1511.3.5 Step-by-Step Instructions

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

1511.3.5.2 Step 2: Understanding the Menu Structure

Before copying the code, understand the accessible menu system:

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#fff', 'primaryBorderColor': '#16A085', 'lineColor': '#16A085', 'secondaryColor': '#E67E22', 'tertiaryColor': '#7F8C8D'}}}%%
flowchart TB
    subgraph MAIN["Main Menu"]
        HOME["Home<br/>(Show Status)"]
        SETTINGS["Settings"]
        STATUS["System Info"]
    end

    subgraph ACCESS["Accessibility Settings"]
        CONTRAST["High Contrast<br/>ON/OFF"]
        TEXT["Large Text<br/>ON/OFF"]
        SOUND["Sound Feedback<br/>ON/OFF"]
        BRIGHT["Brightness<br/>0-100%"]
    end

    subgraph FEEDBACK["Feedback Types"]
        VIS["Visual<br/>(OLED + LED)"]
        AUD["Audio<br/>(Buzzer Tones)"]
        TIME["Timing<br/>(Animation)"]
    end

    SETTINGS --> ACCESS
    HOME --> FEEDBACK
    CONTRAST --> VIS
    SOUND --> AUD

    style MAIN fill:#2C3E50,stroke:#16A085,stroke-width:2px,color:#fff
    style ACCESS fill:#16A085,stroke:#2C3E50,stroke-width:2px,color:#fff
    style FEEDBACK fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff

Figure 1511.2: 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

1511.3.5.3 Step 3: Copy the Accessible Interface Code

Copy the following code into the Wokwi code editor (replace any existing 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();
}

1511.3.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" ] ]
  ]
}

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

1511.3.6 Learning Points

NoteKey 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

1511.3.7 Challenge Exercises

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

1511.3.8 Knowledge Check

WarningTest 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

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

1511.4 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

1511.5 What’s Next

Return to the Interface Design Overview for links to all chapters in this series, or explore the Knowledge Checks to test your understanding.

NoteRelated Chapters