%%{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
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
This lab demonstrates core accessibility concepts:
- Multimodal Feedback: Every action produces visual (OLED + LED), audio (buzzer), and timing-based feedback
- High Contrast Mode: Toggle between standard and high-contrast display modes for users with visual impairments
- Adjustable Text Size: Switch between normal and large text modes for readability
- Audio Confirmation: Distinct sound patterns for navigation, selection, errors, and confirmation
- Progressive Disclosure: Show only relevant options at each menu level to reduce cognitive load
- Persistent Preferences: Save user accessibility settings so they persist across power cycles
- Debounced Input: Handle button bouncing to prevent accidental double-presses
1511.3.3 Circuit Diagram
1511.3.4 Wokwi Simulator Environment
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:
- 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
- Add an SSD1306 OLED Display: Click + and search for “SSD1306”
- Add 4 Push Buttons: Click + and add 4 “Push Button” components
- Add a Passive Buzzer: Click + and search for “Buzzer”
- 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.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
- Press the green Play button to start the simulation
- Observe the home screen showing device status
- Press SELECT to open the main menu
- Use UP/DOWN to navigate between options
- Press SELECT on “Settings” to access accessibility options
- Toggle High Contrast - notice the inverted display colors
- Toggle Large Text - see the text size increase
- Toggle Sound - hear different tones for each action
- Adjust Brightness - cycle through brightness levels
- Press BACK to return to previous menus
1511.3.6 Learning Points
1511.3.7 Challenge Exercises
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
- 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
- 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
- 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
- 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:
- Multimodal Feedback: Every action confirmed through visual, audio, and timing feedback
- High Contrast: Inverted display achieves 21:1 contrast ratio for low vision users
- Persistent Preferences: NVS storage ensures settings survive power cycles
- Button Navigation: Physical controls with debouncing for motor-impaired users
- 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.
- Interface Fundamentals - Core UI patterns
- Multimodal Design - Accessibility principles
- Worked Examples - Voice interface case study
- Process & Checklists - Design validation