Skip to content

15. System Integration

Assignment:

Individual assignment:

  • Design and document the system integration for your final project.

Your project should demonstrate how all the modules developed during the Fab Academy work together as a unified system, including:

  • 2D and 3D design
  • Additive and subtractive fabrication processes
  • Electronics design and production
  • Embedded microcontroller interfacing and programming
  • System integration and packaging

Project Introduction

This week, the objective is to bring together all the components developed throughout the Fab Academy into a single, functional and integrated system.

My final project is an Interactive Physical Dashboard — Map of Côte d'Ivoire, designed for the UNDP Côte d'Ivoire office. This device visualizes the organization's regional actions in real time. It transforms complex development data into a tangible, light-coded experience: a custom PCB shaped like the map of Côte d'Ivoire, embedded in a laser-cut acrylic silhouette, with RGB LEDs positioned at key regional cities. A TFT screen and push buttons complete the interactive interface.

The integrated system includes:

  • An ESP32 microcontroller (Wi-Fi enabled) as the central processing unit
  • A custom PCB shaped like the map of Côte d'Ivoire with WS2812B RGB LEDs at key UNDP city positions
  • A TFT LCD screen to display real-time clock, date, and region information
  • Push buttons (green, black, yellow) for user interaction and mode switching
  • A buzzer for audio feedback
  • A 3D-printed enclosure housing the electronics and screen
  • A laser-cut acrylic map frame to showcase the PCB map
  • A web dashboard (via Wi-Fi) for remote data updates and LED control

Final project — Interactive Map of Côte d'Ivoire


System Architecture

Global Architecture Diagram

┌──────────────────────────────────────────────────────────────┐
│                  WEB DASHBOARD (Wi-Fi / UNDP Data)           │
│          Regional data updates → LED color mapping           │
└─────────────────────────────┬────────────────────────────────┘
                              │ Wi-Fi / WebSocket
┌─────────────────────────────▼────────────────────────────────┐
│                       ESP32 (Main PCB)                       │
│  - Wi-Fi stack    - LED control    - Button handler          │
│  - TFT display    - Real-time clock - Buzzer feedback        │
└───┬──────────┬─────────────┬──────────────────┬─────────────┘
    │          │             │                  │
    ▼          ▼             ▼                  ▼
┌────────┐ ┌────────┐ ┌───────────┐  ┌──────────────────────┐
│  TFT   │ │WS2812B │ │  Buttons  │  │        Buzzer        │
│ Screen │ │ LEDs   │ │ (Green /  │  │   (Audio Feedback)   │
│(Clock /│ │(UNDP   │ │  Black /  │  └──────────────────────┘
│ Date)  │ │ Cities)│ │  Yellow)  │
└────────┘ └────────┘ └───────────┘

System Integration Steps

Step 1 — Electronics Integration

The custom PCB designed during Week 6 (Electronics Design) and produced during Week 8 (Electronics Production) is the heart of the project. It is shaped like the map of Côte d'Ivoire and carries WS2812B RGB LEDs positioned at the coordinates of key UNDP intervention cities.

All components are connected to the ESP32 through the PCB:

Component ESP32 GPIO Protocol
WS2812B LEDs (data) GPIO 13 1-Wire
TFT Screen (SDA/MOSI) GPIO 21 SPI
TFT Screen (SCL/CLK) GPIO 22 SPI
Button Green (Power/On-Off) GPIO 34 Digital
Button Black (Mode) GPIO 35 Digital
Button Yellow (Select/Next) GPIO 32 Digital
Buzzer GPIO 27 Digital

The PCB was designed in KiCad (Week 6) and milled on the Roland SRM-20 (Week 8). It integrates:

  • A 3.3V voltage regulator (SOT-223) for the ESP32 power supply
  • JST connectors for the TFT screen and button harness
  • A 330Ω series resistor on the WS2812B data line
  • USB connector for firmware flashing

Custom PCB — Map shape with WS2812B LEDs


Step 2 — Mechanical Integration (Enclosure & Map Frame)

The mechanical assembly combines two fabrication processes:

3D-Printed Enclosure — designed with SolidWorks (Week 5), printed on the Prusa i4S:

  • Houses the ESP32 PCB, TFT screen, and push buttons
  • Front panel window for the TFT display
  • Recessed openings for the three push buttons
  • Top slot holds the PCB map vertically, backlit by the LEDs

Laser-Cut Acrylic Map Frame — drawn in Inkscape (Week 3), cut on the Epilog Laser:

  • Translucent acrylic cut in the silhouette of Côte d'Ivoire
  • Acts as a light diffuser: when LEDs activate, the acrylic glows and highlights active regions
  • City and region labels engraved directly on the acrylic surface
  • Snaps onto the top of the 3D-printed enclosure via friction-fit slots

Vinyl-cut label (Week 3): The "PRISCA BROU FAB26" label on the front panel was produced with the vinyl cutter.

3D-printed enclosure with acrylic map and TFT screen


Step 3 — Firmware Integration

The embedded firmware was developed using Arduino IDE / PlatformIO (Week 4 — Embedded Programming and Week 11 — Networking and Communications).

The firmware manages all subsystems simultaneously using a non-blocking architecture based on millis() timers.

Firmware Architecture

void loop() {

    // 1. Update real-time clock display on TFT
    updateClock();

    // 2. Check button states and handle mode changes
    handleButtons();

    // 3. Update LED colors based on active mode / Wi-Fi data
    updateLEDs();

    // 4. Handle Wi-Fi and WebSocket communication
    handleWebSocket();

    // 5. Trigger buzzer feedback if needed
    updateBuzzer();
}

LED Regional Mapping Logic

Each WS2812B LED on the PCB corresponds to a UNDP intervention city. The firmware maps city names to LED indices and colors:

  • 🟢 Green → Active UNDP project in the region
  • 🟠 Orange → Project in planning phase
  • 🔴 Red → Region requiring attention / alert
  • 🔵 Blue → Completed project
  • White pulse → Currently selected / highlighted region

When a button is pressed, the system cycles through regions; the selected region's LED pulses white and the TFT displays the city name and project status.

Complete Firmware Code

// undp_civ_dashboard.ino
#include <WiFi.h>
#include <WebSocketsServer.h>
#include <ArduinoJson.h>
#include <Adafruit_NeoPixel.h>
#include <TFT_eSPI.h>

// ========================== NETWORK CONFIGURATION ==========================
const char* SSID_WIFI = "YOUR_SSID";
const char* PASS_WIFI = "YOUR_PASSWORD";

// ========================== PIN CONFIGURATION ==========================
#define PIN_LEDS        13
#define PIN_BTN_GREEN   34
#define PIN_BTN_BLACK   35
#define PIN_BTN_YELLOW  32
#define PIN_BUZZER      27
#define NUM_LEDS        12   // One per key UNDP city

// ========================== GLOBAL OBJECTS ==========================
Adafruit_NeoPixel leds(NUM_LEDS, PIN_LEDS, NEO_GRB + NEO_KHZ800);
TFT_eSPI tft = TFT_eSPI();
WebSocketsServer webSocket = WebSocketsServer(81);

// ========================== CITY / LED MAPPING ==========================
struct City {
    String   name;
    String   status;  // "active", "planning", "alert", "done"
    uint32_t color;
};

City cities[12] = {
    {"Abidjan",      "active",   leds.Color(0,   200, 0)  },
    {"Yamoussoukro", "active",   leds.Color(0,   200, 0)  },
    {"Bouaké",       "planning", leds.Color(255, 140, 0)  },
    {"Daloa",        "active",   leds.Color(0,   200, 0)  },
    {"Korhogo",      "alert",    leds.Color(200, 0,   0)  },
    {"Man",          "planning", leds.Color(255, 140, 0)  },
    {"San-Pédro",    "done",     leds.Color(0,   0,   200)},
    {"Abengourou",   "active",   leds.Color(0,   200, 0)  },
    {"Bondoukou",    "alert",    leds.Color(200, 0,   0)  },
    {"Odienné",      "planning", leds.Color(255, 140, 0)  },
    {"Gagnoa",       "done",     leds.Color(0,   0,   200)},
    {"Divo",         "active",   leds.Color(0,   200, 0)  }
};

int  selectedCity = 0;
unsigned long lastBlink = 0;
bool blinkState = false;

// ========================== DISPLAY ==========================
void showClock() {
    tft.fillRect(0, 0, 240, 60, TFT_BLACK);
    tft.setTextColor(TFT_WHITE, TFT_BLACK);
    tft.setTextSize(3);
    tft.setCursor(10, 10);
    tft.print("18:15:34");
    tft.setTextSize(2);
    tft.setCursor(10, 42);
    tft.print("2026-05-07");
}

void showCity(int idx) {
    tft.fillRect(0, 70, 240, 100, TFT_BLACK);
    tft.setTextColor(TFT_CYAN, TFT_BLACK);
    tft.setTextSize(2);
    tft.setCursor(10, 75);
    tft.print(cities[idx].name);
    tft.setTextColor(TFT_YELLOW, TFT_BLACK);
    tft.setTextSize(1);
    tft.setCursor(10, 100);
    tft.print("Status: " + cities[idx].status);
}

// ========================== LEDS ==========================
void updateLEDs() {
    for (int i = 0; i < NUM_LEDS; i++) {
        if (i == selectedCity) {
            if (millis() - lastBlink > 400) {
                blinkState = !blinkState;
                lastBlink  = millis();
            }
            leds.setPixelColor(i, blinkState ? leds.Color(255, 255, 255) : 0);
        } else {
            leds.setPixelColor(i, cities[i].color);
        }
    }
    leds.show();
}

// ========================== BUTTONS ==========================
void handleButtons() {
    if (digitalRead(PIN_BTN_YELLOW) == LOW) {
        selectedCity = (selectedCity + 1) % NUM_LEDS;
        showCity(selectedCity);
        tone(PIN_BUZZER, 1000, 100);
        delay(200);
    }
    if (digitalRead(PIN_BTN_BLACK) == LOW) {
        selectedCity = (selectedCity - 1 + NUM_LEDS) % NUM_LEDS;
        showCity(selectedCity);
        tone(PIN_BUZZER, 800, 100);
        delay(200);
    }
}

// ========================== WEBSOCKET ==========================
void onWebSocketEvent(uint8_t num, WStype_t type,
                      uint8_t* payload, size_t length) {
    if (type == WStype_TEXT) {
        StaticJsonDocument<256> doc;
        deserializeJson(doc, payload);
        int    ledIdx = doc["led"];
        String stat   = doc["status"].as<String>();
        if (stat == "active")   cities[ledIdx].color = leds.Color(0,   200, 0);
        if (stat == "planning") cities[ledIdx].color = leds.Color(255, 140, 0);
        if (stat == "alert")    cities[ledIdx].color = leds.Color(200, 0,   0);
        if (stat == "done")     cities[ledIdx].color = leds.Color(0,   0, 200);
        cities[ledIdx].status = stat;
        webSocket.broadcastTXT("ok");
    }
}

// ========================== SETUP ==========================
void setup() {
    Serial.begin(115200);

    pinMode(PIN_BTN_GREEN,  INPUT_PULLUP);
    pinMode(PIN_BTN_BLACK,  INPUT_PULLUP);
    pinMode(PIN_BTN_YELLOW, INPUT_PULLUP);
    pinMode(PIN_BUZZER,     OUTPUT);

    leds.begin();
    leds.setBrightness(80);
    leds.show();

    tft.init();
    tft.setRotation(1);
    tft.fillScreen(TFT_BLACK);
    showClock();
    showCity(0);

    WiFi.begin(SSID_WIFI, PASS_WIFI);
    while (WiFi.status() != WL_CONNECTED) { delay(500); }
    Serial.println("[WIFI] IP: " + WiFi.localIP().toString());

    webSocket.begin();
    webSocket.onEvent(onWebSocketEvent);
}

// ========================== LOOP ==========================
void loop() {
    webSocket.loop();
    showClock();
    handleButtons();
    updateLEDs();
}

Step 4 — Software Integration (Web Dashboard)

The web management dashboard was built upon skills acquired during Week 14 (Interface and Application Programming).

It communicates with the ESP32 via WebSocket on port 81 and allows the UNDP team to:

  • View the map of Côte d'Ivoire with live LED status colors for each region
  • Update the status of any city/region (active, planning, alert, done) remotely
  • See the list of UNDP projects per region with a click on the map
  • Export a PDF status report of all regions

The interface is served as a single HTML file hosted on the ESP32 (SPIFFS), requiring no external server — the device is fully autonomous on the local Wi-Fi network.


Step 5 — System Testing

Once all modules were assembled and the firmware uploaded, a series of tests were performed to validate the complete system:

Functional Tests

Test Description Result
LED startup sequence All LEDs light up on boot in sequence Full startup animation
Button navigation Yellow / Black buttons cycle through cities Cities cycle correctly, LED pulses white
TFT clock display Time and date shown on screen Clock displayed correctly (18:15:34 / 2026-05-07)
TFT city info display Selected city name and status shown City name and status update in real time
WebSocket status update Send JSON via dashboard to update LED color LED color changes instantly
Buzzer feedback Button press triggers short beep Audio feedback confirmed
Wi-Fi reconnection Disconnect/reconnect router during operation System reconnects automatically
Acrylic light diffusion LEDs illuminate through the acrylic map frame Regions glow clearly through the acrylic

Bill of Materials (BOM)

Component Reference Quantity Origin Price ($)
ESP32-WROOM-32D U1 1 Digikey $4.08
WS2812B RGB LED SMD (NeoPixel) LED1–LED12 12 Amazon $0.30 x12
TFT LCD Screen 2.4" (ILI9341) TFT1 1 Amazon $8.00
Push Button Green 12mm BTN1 1 Digikey $0.80
Push Button Black 12mm BTN2 1 Digikey $0.80
Push Button Yellow 12mm BTN3 1 Digikey $0.80
Buzzer 5V BZ1 1 Digikey $0.61
Voltage Regulator 3.3V SOT-223 U2 1 Digikey $0.63
PCB FR4 custom (map shape) PCB1 1 FabLab $3.50
3D Printed Enclosure (PLA black) 1 FabLab $4.50
Laser-cut acrylic map (clear) 1 FabLab $3.00
Vinyl label "PRISCA BROU FAB26" 1 FabLab $0.50
Jumper wires & JST connectors FabLab $2.00
TOTAL ~$33.82

Fabrication Processes Summary

Process Tool Used Week
2D Map Design Inkscape Week 2
Vinyl Label Cutting Vinyl cutter Week 3
Acrylic Map Laser Cutting Epilog Laser Week 3
Embedded Programming (ESP32) Arduino IDE Week 4
3D Enclosure Design SolidWorks Week 5
3D Printing (enclosure) Prusa i4S Week 5
PCB Schematic & Layout Design KiCad Week 6
PCB Milling Roland SRM-20 Week 8
Network & WebSocket PlatformIO Week 11
Web Dashboard Interface HTML / JS / WebSocket Week 14

Result

After full integration, the system operates as expected:

  • The Côte d'Ivoire PCB map glows through the laser-cut acrylic frame, with each LED representing a UNDP city in the appropriate status color.
  • The TFT screen displays the real-time clock and the name/status of the currently selected region.
  • The three buttons allow physical navigation between cities and interaction modes.
  • The web dashboard allows the UNDP team to update regional project statuses remotely via Wi-Fi, with LED colors updating instantly.
  • The system is fully autonomous: it hosts its own web interface on the local Wi-Fi network with no external server required.

This project turns complex development data into a tangible, light-coded experience — making UNDP's impact across Côte d'Ivoire visible, interactive, and inspiring.

System fully assembled and operational