Final Project Documentation

 Concept

For my final project, I wanted to build an installation that captures a piece of home: the experience of walking around a stupa in Nepal and spinning the prayer wheels. Growing up, I always loved the peaceful repetition of spinning each wheel, even before I fully understood their spiritual meaning. Over time, I learned that prayer wheels (Mani wheels) are believed to spread blessings and compassion every time they turn.

My goal was to translate that ritual into an interactive digital-physical artwork. The project consists of two main parts:

A physical prayer wheel that plays a sacred “Om” sound when spun.

A digitally illustrated stupa in p5.js, where different architectural sections light up on the physical stupa when touched.

Together, the two elements let the user explore both the ritualistic and symbolic aspects of the stupa.

How the Implementation Works
Interaction Design

The experience begins with the user spinning the prayer wheel. Instead of powering the wheel with a motor, I use the motor as a generator, when the wheel spins, it produces a small voltage. That voltage is read by the Arduino as a signal.

At the same time, the stupa illustration in p5.js acts like an interactive map. When the user touches different physical regions of the stupa (pinnacle, dome, Buddha’s eyes, mandala, etc.) in p5js then p5js sends signal to arudino and light up the parts in physical space.

The design relies on:

Discovery: Users figure out what is touch-sensitive by interacting.

Cultural symbolism: Each part of the stupa has meaning, and the lighting reveals that visually.

Multi-modal feedbackL Sound (prayer wheel), light (stupa), and animation (p5.js).

 

Arudino Code:

My Arduino reads four analog voltages from the prayer wheel motor and also listens for letters sent from p5.js. Each letter corresponds to a part of the stupa that should light up.

// ----- Analog input pins -----
const int volt2 = A2;   // Sensor or voltage input on A2
const int volt3 = A3;   // Sensor or voltage input on A3
const int volt4 = A4;   // Sensor or voltage input on A4
const int volt5 = A5;   // Sensor or voltage input on A5

// ----- Digital output pins (LEDs / relays / indicators) -----
const int pinnacle = 2;
const int thirteen = 3;
const int eye1 = 4;
const int eye2 = 5;
const int dome = 6;
const int mandala = 7;
const int flag = 8;

void setup() {
  // Configure analog input pins
  pinMode(volt2, INPUT);
  pinMode(volt3, INPUT);
  pinMode(volt4, INPUT);
  pinMode(volt5, INPUT);

  // Configure all digital output pins
  pinMode(pinnacle, OUTPUT);
  pinMode(thirteen, OUTPUT);
  pinMode(eye1, OUTPUT);
  pinMode(eye2, OUTPUT);
  pinMode(dome, OUTPUT);
  pinMode(mandala, OUTPUT);
  pinMode(flag, OUTPUT);

  // Start serial communication
  Serial.begin(9600);
}

void loop() {

  // ----- Read all analog inputs -----
  int a2 = analogRead(volt2);
  int a3 = analogRead(volt3);
  int a4 = analogRead(volt4);
  int a5 = analogRead(volt5);

  // ----- Send readings to the Serial Monitor (comma-separated) -----
  Serial.print(a2);
  Serial.print(",");
  Serial.print(a3);
  Serial.print(",");
  Serial.print(a4);
  Serial.print(",");
  Serial.print(a5);
  Serial.print("\n");

  delay(50); // Small delay for stable output



  // ----- Handle incoming serial commands -----
  while (Serial.available() > 0) {

    char message = Serial.read();   // Read one character command

    allOff();   // Always reset all outputs first



    // ----- Activate specific outputs based on incoming character -----
    if (message == 'p') {
      digitalWrite(pinnacle, HIGH);     // Turn on pinnacle
    }
    else if (message == 't') {
      digitalWrite(thirteen, HIGH);     // Turn on thirteen
    }
    else if (message == 'e') {          // "e" turns on both eyes
      digitalWrite(eye1, HIGH);
      digitalWrite(eye2, HIGH);
    }
    else if (message == 'd') {
      digitalWrite(dome, HIGH);         // Dome on
    }
    else if (message == 'm') {
      digitalWrite(mandala, HIGH);      // Mandala on
    }
    else if (message == 'f') {
      digitalWrite(flag, HIGH);         // Flag on
    }
    // Any other character is ignored
  }
}



// ----- Helper function: turn ALL outputs OFF -----
void allOff() {
  digitalWrite(pinnacle, LOW);
  digitalWrite(thirteen, LOW);
  digitalWrite(eye1, LOW);
  digitalWrite(eye2, LOW);
  digitalWrite(dome, LOW);
  digitalWrite(mandala, LOW);
  digitalWrite(flag, LOW);
}

Schematic of the Circuit

p5.js Code Description

The p5.js sketch does these things:

Draws multiple screens (welcome screen → prayer wheel → stupa)

Listens for sensor values sent from Arduino

Plays an “Om” sound only when the wheel spins

Sends letters (‘p’, ‘t’, ‘e’, ‘d’, ‘m’) back to Arduino to activate lights

Handles all on-screen interactions through mouse clicks

code:

// -------------------------------------------------------------
// GLOBAL VARIABLES
// -------------------------------------------------------------

let port;                 // Serial port object for Arduino communication
let button;               // Connect button
let open = false;         // Tracks whether the port is open
let trimvalue;            // Parsed Arduino sensor values
let screen = 1;           // Screen state controller
let sentCommand = false;  // Tracks if Arduino command is already sent
let soundPlaying = false; // Prevents OM sound from retriggering too fast


// -------------------------------------------------------------
// ASSET LOADING (Audio + Images)
// -------------------------------------------------------------
function preload() {

  /////////music////////
  om_sound = loadSound('om.mp3'); // sound from https://pixabay.com/music/search/om/

  // UI Images (all from canva.com as cited)
  welcomescreen = loadImage("startpage.png"); // image from canva.com
  screen2i = loadImage("screen2i.png");       // image from canva.com
  screen3i = loadImage("screen3i.png");       // image from canva.com
  screenpi = loadImage("screenpi.png");       // image from canva.com
  screenmi = loadImage("screenmi.png");       // image from canva.com
  screendi = loadImage("screendi.png");       // image from canva.com
  screenei = loadImage("screenei.png");       // image from canva.com
  screenti = loadImage("screenti.png");       // image from canva.com
}


// -------------------------------------------------------------
// SETUP FUNCTION — Runs once
// -------------------------------------------------------------
function setup() {

  createCanvas(400, 400);

  // Create serial port object (p5.js → Arduino communication bridge)
  port = createSerial();

  // Create connect button
  button = createButton("Connect to Arduino");
  button.position(width / 2 - 50, height / 2);
  button.mousePressed(openArduino); // Attach handler
}


// -------------------------------------------------------------
// OPEN ARDUINO SERIAL PORT
// -------------------------------------------------------------
function openArduino() {

  // If port is not already open, open it
  if (!port.opened()) {

    port.open(9600);   // Must match Arduino baud rate
    open = true;       // Mark port as open
    button.remove();   // Hide button after connecting
  }
}


// -------------------------------------------------------------
// MAIN DRAW LOOP — Runs continuously
// -------------------------------------------------------------
function draw() {

  // Only run UI + sound + sensor logic after port is open
  if (open == true) {

    // ---------------------------------------------------------
    // Screen Navigation
    // ---------------------------------------------------------
    if (screen == 1) {
      welcomescreenf();  // Start page
    }
    else if (screen == 2) {
      screen2f();
    }
    else if (screen == 3) {
      screen3f();
    }
    else if (screen == 4) {
      screenpf();
    }
    else if (screen == 7) {
      screend();
    }
    else if (screen == 8) {
      screenm();
    }

    // ---------------------------------------------------------
    // Read serial input (Arduino → p5.js)
    // ---------------------------------------------------------
    value = port.readUntil("\n");  // Read full sensor line
    port.clear();                  // Clear leftover buffer

    trimvalue = value.trim().split(",");  
    console.log(trimvalue);        // Print array of sensor values


    // ---------------------------------------------------------
    // SOUND TRIGGER LOGIC — OM sound plays when any sensor > 0
    // ---------------------------------------------------------
    if (!soundPlaying) {

      if (
        parseInt(trimvalue[0]) > 0 ||
        parseInt(trimvalue[1]) > 0 ||
        parseInt(trimvalue[2]) > 0 ||
        parseInt(trimvalue[3]) > 0
      ) {
        soundPlaying = true;  // Prevents double-trigger

        om_sound.play();      // Play OM sound

        // Reset lock after sound finishes
        om_sound.onended(() => {
          soundPlaying = false;
        });
      }
    }
  }

  // If port is closed → pause sound
  else {
    om_sound.pause();
  }
}


// -------------------------------------------------------------
// WELCOME SCREEN
// -------------------------------------------------------------
function welcomescreenf() {
  image(welcomescreen, 0, 0, 400, 400);
}


// -------------------------------------------------------------
// MOUSE-PRESSED HANDLER FOR SCREEN NAVIGATION + ARDUINO COMMANDS
// -------------------------------------------------------------
function mousePressed() {

  // ---------------- Screen 1 → Screen 2 -----------------
  if (screen == 1 &&
      mouseX >= 135 && mouseX <= 263 &&
      mouseY >= 354 && mouseY <= 371) {

    screen2f();
  }

  // ---------------- Screen 2 → Screen 3 -----------------
  else if (screen == 2 &&
           mouseX >= 120 && mouseX <= 346 &&
           mouseY >= 192 && mouseY <= 366) {

    screen3f();
  }

  // ---------------- Screen 3 Interactive Hotspots -----------------
  else if (screen == 3) {

    // Pinnacle (Top)
    if (mouseInside(192, 211, 117, 144)) {
      screenpf();  // Arduino: 'p'
    }

    // Thirteen tiers
    else if (mouseInside(185, 225, 147, 178)) {
      screent();   // Arduino: 't'
    }

    // Eyes
    else if (mouseInside(183, 244, 183, 195)) {
      screene();   // Arduino: 'e'
    }

    // Dome
    else if (mouseInside(124, 289, 194, 233)) {
      screend();   // Arduino: 'd'
    }

    // Mandala
    else if (mouseInside(0, 400, 240, 286)) {
      screen = 8;
      screenm();   // Arduino: 'm'
    }
  }

  // ---------------- Back Buttons for All Detail Screens -----------------

  else if (screen == 4 && mouseInside(148, 240, 339, 355)) goBackToMain();
  else if (screen == 5 && mouseInside(126, 274, 302, 325)) goBackToMain();
  else if (screen == 6 && mouseInside(122, 260, 302, 326)) goBackToMain();
  else if (screen == 7 && mouseInside(129, 274, 305, 329)) goBackToMain();
  else if (screen == 8 && mouseInside(115, 259, 304, 325)) goBackToMain();
}


// -------------------------------------------------------------
// HELPERS
// -------------------------------------------------------------

// Reusable function for BACK NAVIGATION
function goBackToMain() {
  port.write(' ');  // Sends "turn everything OFF" to Arduino
  screen = 3;
  screen3f();
}

// Check if mouse is inside a bounding box
function mouseInside(x1, x2, y1, y2) {
  return mouseX >= x1 && mouseX <= x2 &&
         mouseY >= y1 && mouseY <= y2;
}


// -------------------------------------------------------------
// SCREEN FUNCTIONS + ARDUINO COMMANDS
// -------------------------------------------------------------

function screen2f() {
  image(screen2i, 0, 0, 400, 400);
  screen = 2;
}

function screen3f() {
  image(screen3i, 0, 0, 400, 400);
  screen = 3;
}

function screenpf() {
  image(screenpi, 0, 0, 400, 400);
  port.write('p');  // Send “pinnacle”
  screen = 4;
}

function screent() {
  image(screenti, 0, 0, 400, 400);
  port.write('t');
  screen = 5;
}

function screene() {
  image(screenei, 0, 0, 400, 400);
  port.write('e');
  screen = 6;
}

function screend() {
  image(screendi, 0, 0, 400, 400);
  port.write('d');
  screen = 7;
}

function screenm() {
  image(screenmi, 0, 0, 400, 400);
  port.write('m');
  screen = 8;
}

p5js screen:

Arduino  and p5.js Communication

Flow:

User spins the wheel → motor sends voltage

Arduino reads values → prints as CSV string

p5.js reads the CSV → detects movement → plays sound

User clicks a part of the stupa in p5.js → p5 sends a letter

Arduino receives the letter → lights the corresponding LEDs

This loop creates a tight physical and digital connection.

What I’m Proud Of

One of the things I’m most proud of in this project is the way I used a motor as a sensor. In class, we mostly learned how to drive a motor, how to make it spin, how to control its speed, how to power it. But we never talked much about using a motor in reverse, as a generator. The idea actually came from something I learned back in high school: when you spin a motor manually, it becomes a dynamo and creates a small voltage. Remembering that old concept and realizing I could apply it here felt like a huge breakthrough. Instead of attaching extra sensors or complicated hardware, I turned the motor itself into the perfect input device for the prayer wheel. It made the interaction feel more authentic and made me feel resourceful like I made something out of almost nothing.

I’m also proud that the project became more than just a technical assignment. A lot of Arduino + p5.js demos end up being simple lights or sliders, but I wanted my project to feel culturally grounded and emotionally meaningful. Recreating the experience of spinning a prayer wheel and interacting with a stupa allowed me to share a part of Nepalese culture in a way that felt personal. It wasn’t just, “Here’s a sensor and an LED” it was a small spiritual journey for the user. The moment the “Om” sound plays when the wheel turns feels like the installation is breathing with you.

Finally, I’m proud of creating a fully two-way communication system between Arduino and p5.js. At the beginning of the semester, I struggled even to understand how serial communication worked. But in this project, the Arduino and p5.js are constantly talking to each other. Arduino sends sensor data to p5.js, p5.js analyzes it, then sends back precise commands to control the lights on the physical stupa. This feedback loop makes the experience feel alive and responsive. Building this system made me feel like I actually understand how physical computing and digital interaction can merge into one continuous experience.

Overall, the project pushed me technically, creatively, and culturally. It’s the first time I felt like I wasn’t just completing a class assignment. I was creating something that feels like mine.

How This Was Made

I used several tools throughout this project:

Arduino UNO for sensing and controlling LEDs

p5.js for the interactive visuals and sound

Adobe Illustrator / Canva (or whichever tool you used) for drawing the stupa

A small DC motor as a dynamo sensor

WordPress to document the process

Generative AI (ChatGPT) to help debug my Arduino code,  and explain concepts more clearly

The write-up for this project came together very organically. Instead of sitting down and trying to write a perfect report in one go, I started by brainstorming everything in my head and dumping ideas onto paper. I wrote down fragments of thoughts, sketches of memories about stupas and prayer wheels, notes about how the interactions should feel, and even some quick diagrams. It was messy at first, but that process helped me understand what parts of the project mattered the most to me.

From there, I organized the ideas into sections—concept, interaction, technical breakdown, cultural meaning, challenges, and future improvements. I rewrote and refined them little by little. Some parts came from real experiences I’ve had at stupas in Nepal, and others came from experimenting with the Arduino and p5.js until something clicked. Once I had the raw content, I shaped it into the final narrative you see here.

 

Area for future improvement:

I also want to add more lights and more detailed lighting zones on the stupa. At the moment, the LEDs represent the main sections (like the pinnacle, dome, eyes, mandala, etc.), but the lighting could be much richer. I imagine having multiple LEDs in each section, maybe even different colors or subtle animations (like pulsing or fading) to show the sacredness and energy of that part of the structure. More lights would not only make the physical model more visually striking, but also help guide the user’s attention and make the mapping between touch and light feel clearer.

Lastly, I’d like to include more educational content about stupa symbolism. Right now, the project hints at the meanings (like the dome representing the world or the eyes representing wisdom), but I could go deeper. For example, when a section lights up, a short description could appear explaining its spiritual role, history, or connection to Buddhist philosophy. This would turn the installation not just into an interactive artwork, but also a small learning experience about Nepali and Himalayan culture.

Final working video:

Google drive link

Leave a Reply