Week 14: Final Project Report

Concept
I’ve created a unique digital clock that uses ping pong balls arranged in a hexagonal pattern as a display. Each ball is illuminated by an individually addressable LED, creating a distinctive way to show time. The project combines modern technology with an unconventional display method, making it both functional and visually interesting.

The hexagonal layout adds an extra layer of intrigue to the design. Unlike traditional square or rectangular displays, this arrangement creates a honeycomb-like pattern that challenges the conventional perception of digital time display. The use of ping pong balls as diffusers for the LEDs adds a soft, warm glow to each “pixel,” giving the clock a more organic feel compared to harsh LED matrices.

Demo

The initial setup process is designed to be user-friendly while maintaining security. When first powered on, the Wemos Mini creates its own WiFi network. Users need to connect to this temporary network to provide their home WiFi credentials. Once this information is received, the device reboots itself and connects to the specified WiFi network. To complete the setup, users must then connect their device to the same WiFi network. The clock’s IP address is displayed on the hexagonal LED screen, allowing users to easily access the configuration interface through their web browser.

After the connection stage, the user can proceed directly to configuring the clock.

Description of Interaction Design
The interaction design for this clock project focuses on simplicity and intuitiveness, while still offering deep customization options. Users primarily interact with the clock through a web-based interface. The changes apply instantly to the clock face, allowing immediate visual feedback as settings are adjusted. Key interactions include color pickers for customizing the display, sliders for adjusting brightness and animation speed, and dropdown menus for selecting time fonts and background modes such as perlin and gradient. The interface is designed to be responsive, working well on both desktop and mobile browsers. Physical interaction with the clock itself is minimal by design – once set up, it functions autonomously, with all adjustments made through the web interface. This approach ensures that the clock remains an elegant, standalone piece in a user’s space, while still being highly customizable.

Technical Implementation

1. Overview
The heart of the project is a Wemos Mini D1 microcontroller that connects to ntp server for accurate time synchronization. The system uses 128 addressable LEDs arranged in a hexagonal pattern, each LED placed under a cut-up ping pong ball. The entire configuration interface is hosted directly on the Wemos Mini, accessible through any web browser on the local network.

2. Matrix
The core of the project’s display functionality lies in its matrix handling system. The code manages two different display modes: an XY coordinate system and a diagonal system, allowing for flexible number rendering on the hexagonal display.

Here’s the matrix configuration:

#define MX_LED_AMOUNT 128
#define MX_XY_W 39
#define MX_XY_H 13
#define MX_DIAG_W 20
#define MX_DIAG_H 7

The display uses lookup tables to map logical positions to physical LED numbers. This is crucial for the hexagonal layout:

static const uint8_t xyLEDPos[MX_DIAG_H][MX_XY_W] = {
    {0, 0, 0, 13, 0, 14, 0, 27, 0, 28, 0, 41, 0, 42, 0, 55, 0, 56, 0, 69, 0, 70, 0, 83, 0, 84, 0, 97, 0, 98, 0, 111, 0, 112, 0, 125, 0, 0, 0},
    {0, 0, 2, 0, 12, 0, 15, 0, 26, 0, 29, 0, 40, 0, 43, 0, 54, 0, 57, 0, 68, 0, 71, 0, 82, 0, 85, 0, 96, 0, 99, 0, 110, 0, 113, 0, 124, 0, 0},
    {0, 3, 0, 11, 0, 16, 0, 25, 0, 30, 0, 39, 0, 44, 0, 53, 0, 58, 0, 67, 0, 72, 0, 81, 0, 86, 0, 95, 0, 100, 0, 109, 0, 114, 0, 123, 0, 126, 0},
    {1, 0, 4, 0, 10, 0, 17, 0, 24, 0, 31, 0, 38, 0, 45, 0, 52, 0, 59, 0, 66, 0, 73, 0, 80, 0, 87, 0, 94, 0, 101, 0, 108, 0, 115, 0, 122, 0, 127},
    {0, 5, 0, 9, 0, 18, 0, 23, 0, 32, 0, 37, 0, 46, 0, 51, 0, 60, 0, 65, 0, 74, 0, 79, 0, 88, 0, 93, 0, 102, 0, 107, 0, 116, 0, 121, 0, 128, 0},
    {0, 0, 6, 0, 8, 0, 19, 0, 22, 0, 33, 0, 36, 0, 47, 0, 50, 0, 61, 0, 64, 0, 75, 0, 78, 0, 89, 0, 92, 0, 103, 0, 106, 0, 117, 0, 120, 0, 0},
    {0, 0, 0, 7, 0, 20, 0, 21, 0, 34, 0, 35, 0, 48, 0, 49, 0, 62, 0, 63, 0, 76, 0, 77, 0, 90, 0, 91, 0, 104, 0, 105, 0, 118, 0, 119, 0, 0, 0},
};

static const uint8_t diagonalLEDPos([MX_DIAG_H][MX_DIAG_W] = {
    {13, 14, 27, 28, 41, 42, 55, 56, 69, 70, 83, 84, 97, 98, 111, 112, 125, 0, 0, 0},
    {2, 12, 15, 26, 29, 40, 43, 54, 57, 68, 71, 82, 85, 96, 99, 110, 113, 124, 0, 0},
    {3, 11, 16, 25, 30, 39, 44, 53, 58, 67, 72, 81, 86, 95, 100, 109, 114, 123, 126, 0},
    {1, 4, 10, 17, 24, 31, 38, 45, 52, 59, 66, 73, 80, 87, 94, 101, 108, 115, 122, 127},
    {0, 5, 9, 18, 23, 32, 37, 46, 51, 60, 65, 74, 79, 88, 93, 102, 107, 116, 121, 128},
    {0, 0, 6, 8, 19, 22, 33, 36, 47, 50, 61, 64, 75, 78, 89, 92, 103, 106, 117, 120},
    {0, 0, 0, 7, 20, 21, 34, 35, 48, 49, 62, 63, 76, 77, 90, 91, 104, 105, 118, 119},
};

The code includes helper functions to convert between coordinate systems:

int matrix::ledXY(int x, int y) {
    if (x < 0 || y < 0 || x >= MX_XY_W || y >= MX_XY_H) return -1;
    return ((y & 1) ? 0 : xyLEDPos[y >> 1][x]) - 1;
}

int matrix::ledDiagonal(int x, int y) {
    if (x < 0 || y < 0 || x >= MX_DIAG_W || y >= MX_DIAG_H) return -1;
    return diagonalLEDPos[y][x] - 1;
}

This matrix system allows for efficient control of the LED display while abstracting away the complexity of the physical layout. The code handles the translation between logical positions and physical LED addresses, making it easier to create patterns and display numbers on the hexagonal grid.

3. Fonts
A crucial part of the clock’s functionality is its ability to display numbers clearly on the hexagonal LED matrix. To achieve this, the project uses custom font definitions for both the XY and diagonal coordinate systems. These fonts are optimized for the unique layout of the display.

Here’s the font definition for the XY and diagonal coordinate system:

const uint8_t font_xy[] PROGMEM = {
    0x03, 0x09, 0x12, 0x18,  // 0 (15)
    0x00, 0x03, 0x0c, 0x10,  // 1 (16)
    0x01, 0x0d, 0x16, 0x10,  // 2 (17)
    0x01, 0x05, 0x16, 0x18,  // 3 (18)
    0x03, 0x04, 0x06, 0x18,  // 4 (19)
    0x03, 0x05, 0x14, 0x18,  // 5 (20)
    0x03, 0x0d, 0x14, 0x18,  // 6 (21)
    0x01, 0x01, 0x1e, 0x00,  // 7 (22)
    0x03, 0x0d, 0x16, 0x18,  // 8 (23)
    0x03, 0x05, 0x16, 0x18,  // 9 (24)
};

const uint8_t font_diagonal[] PROGMEM = {
    0x0f, 0x11, 0x1e,  // 0 (15)
    0x00, 0x02, 0x1f,  // 1 (16)
    0x0d, 0x15, 0x16,  // 2 (17)
    0x09, 0x15, 0x1e,  // 3 (18)
    0x03, 0x04, 0x1f,  // 4 (19)
    0x13, 0x15, 0x19,  // 5 (20)
    0x0f, 0x15, 0x18,  // 6 (21)
    0x01, 0x01, 0x1e,  // 7 (22)
    0x0f, 0x15, 0x1e,  // 8 (23)
    0x03, 0x15, 0x1e,  // 9 (24)
};

These font definitions are stored in program memory (PROGMEM) to save RAM. Each number is represented by a series of bytes that define which LEDs should be lit to form the digit. The XY font uses a 4×5 grid, while the diagonal font uses a 3×5 grid, both optimized for the hexagonal layout.

4. Color palette
The visual appeal of the clock comes from its rich color palette system, implemented using FastLED’s gradient palette functionality. These palettes define how colors transition across the LED display, creating dynamic and engaging visual effects.

Here’s how the color palettes are defined:

#include <FastLED.h>

DEFINE_GRADIENT_PALETTE(FireGrad) {
    0, 0, 0, 0,
    128, 255, 0, 0,
    224, 255, 255, 0,
    255, 255, 255, 255
};

DEFINE_GRADIENT_PALETTE(SunsetGrad) {
    0, 120, 0, 0,
    22, 179, 22, 0,
    51, 255, 104, 0,
    85, 167, 22, 18,
    135, 100, 0, 103,
    198, 16, 0, 130,
    255, 0, 0, 160
};

Each palette is defined with specific color points and their positions in the gradient. For example, the Fire gradient transitions from black (0,0,0) through red and yellow to white, creating a realistic flame effect. The numbers represent positions (0-255) and RGB values for each color point.

5. Draw function
The drawClock() function is the core of the clock’s display functionality. It handles the rendering of time on the LED matrix, accommodating different display styles and synchronization states.

static void draw() {
    uint8_t font = db[clock_style].toInt();
    if (!font) return;

    matrix.setModeDiagonal();

The function starts by retrieving the clock style from the database and setting the matrix to diagonal mode.

if (!NTP.synced()) {
    matrix.setFont(font_xy);
    matrix.setCursor(1, 1);
    matrix.print("--");
    matrix.setCursor(12, 1);
    matrix.print("--");
    return;
}

If the clock isn’t synchronized with NTP (Network Time Protocol), it displays dashes instead of numbers.

The main part of the function uses a switch statement to handle different clock styles:

switch (db[clock_style].toInt()) {
    case 1:
        // 3x5 font, standard layout
    case 2:
        // 3x5 diagonal font
    case 3:
        // 4x5 font, split hour and minute digits
}

Each case represents a different display style, using various fonts and layouts. For example:

case 2:
    matrix.setFont(font_3x5_diag);

    matrix.setCursor(1, 1);
    if (dt.hour < 10) matrix.print(' ');
    matrix.print(dt.hour);

    matrix.setCursor(11, 1);
    if (dt.minute < 10) matrix.print(0);
    matrix.print(dt.minute);

    dots(9, 9);
    break;

This case uses a diagonal 3×5 font, positions the cursor for hours and minutes, and adds leading spaces or zeros for single-digit values. The dots() function adds separator dots between hours and minutes.

6. Background effect

Gradient(int x0, int y0, int w, int h, int angle) {
    uint16_t hypot = sqrt(w * w + h * h) / 2;
    cx = x0 + w / 2;
    cy = y0 + h / 2;
    sx = cos(radians(angle)) * hypot;
    sy = sin(radians(angle)) * hypot;
    len = sqrt(sx * sx + sy * sy) * 2;
}

This code defines a constructor for the Gradient class, which calculates parameters needed for creating gradient color effects. It takes initial coordinates (x0, y0), width (w), height (h), and an angle as inputs. The constructor first calculates half the hypotenuse, then determines the center point of the rectangle (cx, cy) and calculates the x and y components (sx, sy) of the gradient vector using trigonometry, where the angle is converted from degrees to radians. The length (len) of the gradient is computed as twice the magnitude of this vector.

for (int y = 0; y < matrix.height(); y++) {
    for (int x = 0; x < matrix.width(); x++) {
        if (matrix.xyLED(x, y) < 0) continue;
        uint32_t col = getPaletteColor(palette, inoise16(x * scale * 64, y * scale * 64, count * 32), bright);
        matrix.setLED(x, y, col);
    }
}

This code snippet controls the visual effects on the LED matrix by creating a dynamic noise-based pattern. It iterates through each position in the matrix using nested loops for x and y coordinates. For each valid LED position (checked using matrix.xyLED(x, y)), it generates a color using Perlin noise (inoise16). The noise function takes the x and y coordinates (scaled by 64) and a time-based count variable to create movement. The getPaletteColor function then maps this noise value to a color from the current palette, taking into account the brightness level (bright). Finally, each LED in the matrix is set to its calculated color, creating a smooth, flowing animation effect across the display. This is what gives the clock its dynamic, animated background patterns.

7. Server parsing

function createURL(endpoint, queryParams = {}) {
    // Get the origin of the current window (protocol + host)
    const origin = window.location.origin;

    // Start building the URL with the endpoint
    let url = origin + "/" + endpoint;
    
    // A flag to determine if the first query parameter is being added
    let isFirstParam = true;

    // Iterate over the query parameters object
    for (let key in queryParams) {
        // Only add parameters that are not null
        if (queryParams[key] !== null) {
            // Append '?' for the first parameter or '&' for subsequent parameters
            url += isFirstParam ? "?" : "&";
            isFirstParam = false; // Set the flag to false after the first parameter
            
            // Append the key-value pair to the URL
            url += key + "=" + queryParams[key];
        }
    }

    // Return the constructed URL
    return url;
}

This code defines a createURL method that constructs a complete URL for API endpoints. It starts by getting the current window’s origin (protocol and host) and builds upon it. The method accepts an endpoint parameter and an optional queryParams object. Then method then constructs the URL by combining the origin with the endpoint, and systematically adds any query parameters from the queryParams object. It handles the proper formatting of query parameters by adding ‘?’ for the first parameter and ‘&’ for subsequent ones, but only includes parameters that aren’t null. This ensures that all URLs are properly formatted with the correct separators between parameters.

8. Send function

async function send(action, id = null, value = null) {
    const TIMEOUT_MS = 2000;
    const BINARY_MARKER = "__BSON_BINARY";

    // Helper function to combine two bytes into a 16-bit unsigned integer
    function combineBytes(byte1, byte2) {
        return ((byte1 << 8) | byte2) >>> 0;
    }

    // Helper function to escape special characters in strings
    function escapeString(str) {
        return str.replaceAll(/([^\\])\\([^\"\\nrt])/gi, "$1\\\\$2")
            .replaceAll(/\t/gi, "\\t")
            .replaceAll(/\n/gi, "\\n")
            .replaceAll(/\r/gi, "\\r")
            .replaceAll(/([^\\])(")/gi, '$1\\"');
    }

    try {
        // Attempt to fetch data with a timeout
        const response = await fetch(this.makeUrl("settings", { action, id, value }), {
            signal: AbortSignal.timeout(TIMEOUT_MS)
        });

        if (!response || !response.ok) return null;

        const data = new Uint8Array(await response.arrayBuffer());
        if (!data.length) return {};

        let jsonString = "";
        let binaryData = [];
        
        // Parse the binary data
        for (let i = 0; i < data.length; i++) {
            const typeBits = 224 & data[i];
            const valueBits = 31 & data[i];

            switch (typeBits) {
                case 192: // Object/Array start/end
                    if (8 & valueBits) {
                        jsonString += 16 & valueBits ? "{" : "[";
                    } else {
                        jsonString = jsonString.replace(/,$/, '');
                        jsonString += (16 & valueBits ? "}" : "]") + ",";
                    }
                    break;

                case 0: // Key (from dictionary)
                case 64: // Value (from dictionary)
                    jsonString += `"${y[combineBytes(valueBits, data[++i])]}"${typeBits == 0 ? ":" : ","}`;
                    break;

                case 32: // Key (string)
                case 96: // Value (string)
                    {
                        const length = combineBytes(valueBits, data[++i]);
                        i++;
                        const str = escapeString(new TextDecoder().decode(data.slice(i, i + length)));
                        jsonString += `"${str}"${typeBits == 32 ? ":" : ","}`;
                        i += length - 1;
                    }
                    break;

                case 128: // Number
                    {
                        const isNegative = 16 & valueBits;
                        const byteCount = 15 & valueBits;
                        let num = BigInt(0);
                        for (let j = 0; j < byteCount; j++) {
                            num |= BigInt(data[++i]) << BigInt(8 * j);
                        }
                        jsonString += `${isNegative ? "-" : ""}${num},`;
                    }
                    break;

                case 160: // Float
                    {
                        let floatBits = 0;
                        for (let j = 0; j < 4; j++) {
                            floatBits |= data[++i] << (8 * j);
                        }
                        const float = new Float32Array(new Uint32Array([floatBits]).buffer)[0];
                        jsonString += isNaN(float) ? '"NaN"' : 
                                      isFinite(float) ? float.toFixed(valueBits) : 
                                      '"Infinity"';
                        jsonString += ",";
                    }
                    break;

                case 224: // Binary data
                    {
                        const length = combineBytes(valueBits, data[++i]);
                        i++;
                        jsonString += `"${BINARY_MARKER}#${binaryData.length}",`;
                        binaryData.push(data.slice(i, i + length));
                        i += length - 1;
                    }
                    break;
            }
        }

        // Remove trailing comma if present
        jsonString = jsonString.replace(/,$/, '');

        // Parse JSON and replace binary placeholders
        const parsedJson = JSON.parse(jsonString);

        function replaceBinaryPlaceholders(obj) {
            if (typeof obj !== 'object' || obj === null) return;
            
            for (const [key, value] of Object.entries(obj)) {
                if (typeof value === 'object') {
                    replaceBinaryPlaceholders(value);
                } else if (typeof value === 'string' && value.startsWith(BINARY_MARKER)) {
                    const index = parseInt(value.split("#")[1]);
                    obj[key] = binaryData[index];
                }
            }
        }

        replaceBinaryPlaceholders(parsedJson);
        return parsedJson;

    } catch (error) {
        console.error("Error in sendAndParse:", error);
        return null;
    }
}

This function is an asynchronous method that sends a request to a server and processes the response using a custom binary format. It begins by making a fetch request with specified action, id, and value parameters, setting a 2-second timeout. Upon receiving the response, the function parses the binary data into a JSON structure, handling various data types such as objects, arrays, strings, numbers, floats, and binary data. This function essentially combines network communication with complex data parsing and transformation in a single, comprehensive operation.

Schematic

Achievements
The most satisfying aspect of this project is how the hexagonal display turned out. Despite challenges with LED spacing, ping pong ball imperfections, and a lot of bugs in the code, the final display creates clear, readable numbers while maintaining an artistic quality. The wireless configuration system also worked better than expected, making the clock truly standalone after initial setup.

Future Improvements
Several areas could be enhanced in future iterations. The initial WiFi setup process could be streamlined, perhaps using WPS or a QR code system. The ping pong ball mounting system could be redesigned to better hide the seam lines and create more uniform light diffusion. Adding additional display modes and animations would also make the clock more versatile or even adding a ticker tape and maybe even some games.

Week 13: Final Project User Testing

User Testing Experience
I conducted a user test with my friend, creating a scenario where he had to interact with the clock from scratch. I completely reset the device and watched how he would handle the initial setup and configuration process.

Clock Setup

Clock interaction

What I Observed
The first hurdle came with the connection process. Since the clock needs to connect to WiFi and sync with the time server, this part proved to be a bit challenging for a first-time user. My friend struggled initially to understand that he needed to connect to the clock’s temporary WiFi network before accessing the configuration interface. This made me realize that this step needs better explanation or perhaps a simpler connection method.
However, once past the connection stage, things went much smoother. The configuration interface seemed intuitive enough – he quickly figured out how to adjust the display settings and customize the clock face. The color selection and brightness controls were particularly straightforward, and he seemed to enjoy experimenting with different combinations.

Key Insights
The most interesting part was watching how he interacted with the hexagonal display. The unusual arrangement of the ping pong balls actually made him more curious about how the numbers would appear. He mentioned that comparing different fonts was satisfying , especially with the lighting background effects.

Areas for Improvement
The initial WiFi setup process definitely needs work. I’m thinking about adding a simple QR code on the device that leads directly to the configuration page, or perhaps creating a more streamlined connection process. Also, some basic instructions printed on the device itself might help first-time users understand the setup process better.
The good news is that once configured, the clock worked exactly as intended, and my friend found the interface for making adjustments quite user-friendly. This test helped me identify where I need to focus my efforts to make the project more accessible to new users while keeping the features that already work well.

Moving Forward
This testing session was incredibly valuable. It showed me that while the core functionality of my project works well, the initial user experience needs some refinement. For the future I will focus particularly on making the setup process more intuitive for first-time users.

Week 12: Final Project Progress Report

Final Concept
I’m building an interactive clock that uses ping pong balls as display elements. The main feature is a hexagonal matrix made of 128 ping pong balls that will show the current time with different cool background effects which you can control from your phone. Each ball will have an addressable LED inside that can be individually controlled.

Wemos Mini (Arduino)
The Wemos Mini D1 will be the brain of the operation, handling way more than I initially planned.

Main Functions:

  • Connect to WiFi to get accurate time from pool.ntp.org
  • Control all 128 WS2812B LEDs
  • Handle all time calculations and display patterns
  • Store user settings in its memory

Schematic

P5.js
P5.js will serve as a configuration interface for the clock. It will only send the data entered as it is to the Wemos Mini.

Configuration options:

  • Font and font color
  • Background effect
    • Type (Perlin/Gradient)
    • Color palette
    • Brightness
    • Speed
    • Angle
  • Overall brightness of the clock
  • Night mode (ON/OFF)
  • Time server and timezone
  • WiFi settings

For this project I want to try hosting the P5.js sketch on the Wemos Mini, making it accessible through any web browser on the local network.This task sounds to me like it would be a very interesting one to explore.

Challenges Faced
My first major hurdle came from the LED strip specifications. I got the WS2812B LED Strip rated at 30 LEDs per meter, but the actual spacing turned out to be 33.15mm instead of the expected 33.33mm per LED. This small 0.18mm difference created significant alignment issues over the full length of the strip.

My second challenge was the ping pong balls, they weren’t quite what I expected either. Despite ordering balls without logos, each one came with a printed logo on them, which could not be rubbed off with anything, and using sandpaper would cause light diffusion. On top of that, I discovered that each ball wasn’t a single piece as I’d assumed, but actually two halves merged together. This created a visible seam when light shone through the ball.

I found myself at a crossroads: I could either align all the balls so the seams were positioned horizontally, which would make the logos unpossible to cut out because they were placed randomly on the ball, or I could prioritize hiding the logos, which would result in the seams being more visible. After some consideration, I decided to cut out the logos. They were more distracting than the seams, and I figured the uniform light would help mask the seam lines better than it would hide the printed logos.

These unexpected issues with both the LEDs and the ping pong balls have forced me to rethink my entire mounting and display system. It’s been frustrating, but it’s also pushing me to come up with more creative solutions.

Next steps

  • Set up the Wemos Mini web server
  • Create the P5.js configuration interface
  • Design the LED patterns for numbers
  • Build a proper mounting system that accounts for actual LED spacing
  • Test WiFi connectivity and time synchronization

Despite these setbacks, I’m still excited about this project. Each challenge is teaching me something new, and I’m looking forward to seeing how the final product turns out.

Reading Reflection – Week 11

The articles profoundly challenged my assumptions about interaction design, particularly regarding our overreliance on flat touchscreen interfaces. While I’ve always appreciated the sleek aesthetics of modern devices, I now recognize how we’ve sacrificed the rich tactile experiences our hands are capable of experiencing.

The discussion about the trade-offs between physical and touch interfaces resonates strongly with my own experiences in technology. Like many others, I’ve noticed the satisfying feedback of mechanical keyboards versus the hollow experience of typing on glass surfaces. This observation extends beyond personal preference – it reflects a fundamental human desire for tactile feedback that current touch interfaces often fail to provide. In my own projects, I’m now exploring ways to incorporate haptic feedback and physical controls that complement, rather than replace, touch interfaces, understanding that different interaction methods serve different purposes and contexts.

The vision of future interfaces that better adapt to human capabilities has inspired me to think more boldly about interaction design. Rather than accepting the limitations of current technology, I’m now exploring how to create interfaces that engage multiple senses and leverage our natural ability to manipulate objects in three-dimensional space. This could mean developing prototypes that combine touch interfaces with physical controls, or experimenting with new forms of haptic feedback that provide more nuanced physical responses. The articles have helped me understand that the future of interaction design isn’t about choosing between physical and digital interfaces, but rather about finding innovative ways to blend them together to create more intuitive and satisfying user experiences.

Week 11: In Class Exercise

The provided code snippets include p5.js sketches and Arduino code for exercises completed during our class sessions. In these exercises, a potentiometer was utilized as the input sensor.

Exercise #1
Arduino code:

void setup() {
  // Initialize serial communication
  Serial.begin(9600);  
}

void loop() {
  // Read analog value from potentiometer on pin A0
  int sensorValue = analogRead(A0);
  // Print the sensor value to serial monitor
  Serial.println(sensorValue);
  delay(50);
}

P5.js code:

let serial;
let value = 0;

function setup() {
  createCanvas(800, 400);
}

function draw() {
  background(220);
  
  if (!serialActive) {
    text("Press Space Bar to select Serial Port", 20, 30);
  } else {
    text("Connected", 20, 30);
    
    // Display current sensor value
    text('value = ' + str(value), 20, 50);
  }
  
  // Map sensor value to canvas width for circle position
  let xPos = map(value, 0, 1023, 0, width);
  fill(255, 0, 0);
  
  // Draw circle at mapped position
  ellipse(xPos, height/2, 50, 50);  
}

function keyPressed() {
  if (key == " ") {
    // Initialize serial connection when spacebar is pressed
    setUpSerial();  
  }
}

function readSerial(data) {
  if (data != null) {
    // Split incoming data
    let fromArduino = split(trim(data), ",");  
    if (fromArduino.length == 1) {
      // Update value with received data
      value = int(fromArduino[0]);  
    }
    // Prepare mouse Y position to send
    let sendToArduino = mouseY + "\n";  
    // Send data to Arduino
    writeSerial(sendToArduino);  
  }
}

Demo:

Exercise #2
Arduino code:

// Define the pin for LED
const int ledPin = 11;

void setup() {
  Serial.begin(9600);
  
  // Set built-in LED as output
  pinMode(LED_BUILTIN, OUTPUT);  
  
  // Set external LED pin as output
  pinMode(ledPin, OUTPUT); 
  
  // Quick flash of external LED to indicate startup
  digitalWrite(ledPin, HIGH);
  delay(200);
  digitalWrite(ledPin, LOW);
  
  // Handshake loop: blink built-in LED and send "0" until serial data is received
  while (Serial.available() <= 0) {
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("0");
    delay(300);
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}

void loop() {
  while (Serial.available()) {
    // Turn on built-in LED when receiving data
    digitalWrite(LED_BUILTIN, HIGH);  
    
    // Read brightness value from serial
    int brightness = Serial.parseInt();
    if (Serial.read() == '\n') {
      // Set LED brightness
      analogWrite(ledPin, brightness);  
      delay(5);
    }
  }
}

P5.js code:

let serial;
let value = 0;
let brightness = 0;

function setup() {
  createCanvas(800, 400);
}

function draw() {
  background(220);
  
  if (!serialActive) {
    // Display instruction if serial connection is not active
    text("Press Space Bar to select Serial Port", 20, 30);
  } else {
    // Display connection status and current values
    text("Connected", 20, 30);
    text('Brightness = ' + str(brightness), 20, 50);
    text('Value = ' + str(value), 20, 70);
  }

  // Map mouseY position to brightness value
  brightness = map(mouseY, 0, height, 0, 125);
  
  // Visual feedback: draw rectangle with current brightness
  fill(brightness);
  rect(100, 100, 200, 200);
}

function keyPressed() {
  if (key == " ") {
    // Start serial connection when spacebar is pressed
    setUpSerial();
  }
}

function readSerial(data) {
  if (data != null) {
    // Parse incoming data from Arduino
    let fromArduino = split(trim(data), ",");
    if (fromArduino.length == 1) {
      value = int(fromArduino[0]);
    }
    
    // Send brightness value to Arduino
    let sendToArduino = int(brightness) + "\n";
    writeSerial(sendToArduino);
  }
}

Demo:

Exercise #3
Arduino code:

// Define LED pin
const int LED_PIN = 2;    
// Define analog input pin
const int P_PIN = A0;     

void setup() {
  Serial.begin(9600);

  // Set LED pin as output
  pinMode(LED_PIN, OUTPUT);

  // Initially turn off LED
  digitalWrite(LED_PIN, LOW); 

  // Wait for the handshake
  while (Serial.available() <= 0) {
    Serial.println("0");
  }
}

void loop() {
  if (Serial.available()) {
    // Read integer from serial
    int bounce = Serial.parseInt();  
    if (Serial.read() == '\n') { 
       // Set LED state based on input from p5.js
      digitalWrite(LED_PIN, bounce);
    }
  }

   // Read and send analog value
  Serial.println(analogRead(P_PIN));
  
  // Short delay to prevent flooding the serial port
  delay(5);  
}

P5.js code:

let velocity, gravity, position, acceleration, wind;
let sensorData = 0;
const drag = 0.99;
let mass = 50;
let bounce = 0;

function setup() {
  createCanvas(640, 360);
    
  // Initialize vectors for physics simulation
  position = createVector(width/2, 0);
  velocity = createVector(0,0);
  acceleration = createVector(0,0);
  gravity = createVector(0, 0.5*mass);
  wind = createVector(0,0);
}

function draw() {
  background(255);
  
  if (!serialActive) {
    text("Press Space Bar to select Serial Port", 20, 30);
  } else {
    text("Connected", 20, 30);
    text('Sensor = ' + str(sensorData), 20, 50);
    
    // Apply forces when serial is active
    applyForce(wind);
    applyForce(gravity);
  }

  if (serialActive) {
    // Map sensor data to wind force
    wind.x = map(sensorData, 0, 1023, -2, 2);

    // Physics simulation
    applyForce(wind);
    applyForce(gravity);
    velocity.add(acceleration);
    velocity.mult(drag);
    position.add(velocity);
    acceleration.mult(0);

    // Draw the object
    ellipse(position.x, position.y, mass, mass);

    // Handle bouncing
    if (position.y > height-mass/2) {
      velocity.y *= -0.9;
      position.y = height-mass/2;
      bounce = 1;
    } else {
      bounce = 0;
    }
  }
}

// Function to apply force to the object
function applyForce(force) {
  let f = p5.Vector.div(force, mass);
  acceleration.add(f);
}

function keyPressed() {
  if (key == "b") {
    // Reset object with random mass
    mass = random(10,50);
    position.y = -mass;
    velocity.mult(0);
  }
  
  if (key == " ") {
    // Start serial connection
    setUpSerial();
  }
}

// Handle serial communication
function readSerial(data) {
  if (data != null) {
    let fromArduino = split(trim(data), ",");
    if (fromArduino.length == 1) {
      sensorData = int(fromArduino[0]);
    }
    
    // Send bounce state to Arduino
    let sendToArduino = int(bounce) + "\n";
    writeSerial(sendToArduino);
  }
}

Demo:

Final Project: Initial Idea

Inspiration and Concept
For my final project, I want to create a unique clock display using ping pong balls. I got this idea after noticing different types of watches people have in their homes and thinking about how we could display time in a non-traditional way.

Instead of using regular digital numbers or clock hands, my project will use ping pong balls that light up to show the time. What makes this interesting is that the balls won’t be arranged in a typical square grid – they’ll form a hexagonal pattern, kind of like a honeycomb. This creates a cool challenge because I’ll need to figure out how to display numbers using this unusual arrangement.

Next Steps

  • Design the physical layout of the ping pong balls
  • Figure out how to arrange LEDs inside each ball
  • Write code to display numbers in a hexagonal pattern
  • Set up communication between P5 and Arduino

I’m excited about this project because it combines physical objects with digital control in a way that’s both practical and visually interesting. Plus, it’s something that could actually be useful in someone’s home, not just a tech demo.

This feels like a good challenge because I’ll need to solve both hardware problems (how to mount and light up the balls) and software issues (how to display numbers in this weird layout). I think it’ll be fun to see it all come together!

Reading Reflection – Week 10

A new angle on creating meaningful interactions is provided by Tom Igoe’s observations on interactive art and physical computing. One crucial takeaway is his recommendation to let individuals participate in a project in their own way rather than directing every detail. It can be tempting to give directions or explanations in interactive art, but Igoe contends that doing so can restrict the audience’s creativity. This method emphasizes how crucial it is to give consumers room to explore and interpret on their own terms, which will make the experience more memorable and intimate.

I became aware of the importance of basic, intuitive actions in design after seeing Igoe’s examples of ordinary gestures—such as tapping or moving through a space—used as interactive features. People can interact with technology naturally when these well-known motions are turned into interesting experiences. A project that combines commonplace activities with artistic involvement, such as one in which a person’s movement or touch activates music or graphics, seems both familiar and unexpected. It helps me consider how I may use such movements in my projects to produce interactions that seem natural and grab viewers’ interest.

My comprehension of user-centered design is further enhanced by his analogy between creating interactive art and directing a play. A skilled director creates the scene yet lets the actor interpret and react freely, not controlling every step. Similarly, creating a project that allows for user exploration changes the emphasis from the designer’s intention to the user’s experience, making every interaction special. In the future, I hope to develop designs that lead users through subliminal clues, empowering them to come to their own conclusions and derive personal meaning, transforming the encounter into a cooperative dialogue.

Week 10: Musical Instrument

Concept
The concept behind this project is inspired by the theremin, one of the earliest electronic musical instruments. Unlike the traditional theremin that uses electromagnetic fields, this version employs an ultrasonic sensor to detect hand movements and converts them into musical notes from the A minor pentatonic scale.

Schematic

Code snippet
The implementation uses an Arduino with an ultrasonic sensor (HC-SR04) and a piezo buzzer. Here’s the key components of the code:

const int SCALE[] = {
    147, 165, 196, 220, 262, 294, 330, 392, 440,
    523, 587, 659, 784, 880, 1047, 1175, 1319, 1568,
    1760, 2093, 2349
};

The scale array contains frequencies in Hertz, representing notes in the A minor pentatonic scale, spanning multiple octaves. This creates a musical range that’s both harmonious and forgiving for experimentation.

The system operates in two phases:

Calibration Phase

void calibrateSensor() {
    unsigned long startTime = millis();
    while (millis() - startTime < CALIBRATION_TIME) {
        int distance = measureDistance();
        maxDistance = max(maxDistance, distance);
    }
}

Performance Phase

void loop() {
    int currentDistance = measureDistance();
    int mappedNote = map(currentDistance, MIN_DISTANCE, maxDistance, 
                        SCALE[0], SCALE[SCALE_LENGTH - 1]);
    int nearestNote = findNearestNote(mappedNote);
    tone(PIEZO_PIN, nearestNote);
    delay(30);
}

Demo

When powered on, the instrument takes 5 seconds to calibrate, determining the maximum distance it will respond to. Moving your hand closer to the sensor produces higher pitches, while moving away produces lower ones. The pentatonic scale ensures that all notes work harmoniously together, making it easier to create pleasing melodies.

Reflection and Future Improvements
Current Limitations:

  • The response time has a slight delay due to sensor readings
  • Sound quality is limited by the piezo buzzer
  • Only supports single notes at a time

Potential Enhancements:

  1. Replace the piezo with a better quality speaker
  2. Add an amplifier circuit for improved sound output
  3. Incorporate multiple sensors for more control dimensions

Week 9: Adaptive Lighting System

Concept
This project explores the interaction between analog and digital sensing to create an adaptive lighting system. The system uses an photoresistor as an analog sensor to measure ambient light levels, converting physical light quantities into continuous electrical signals. A push button serves as our digital sensor, providing discrete binary input. These sensors control two LEDs – one with PWM for variable brightness and another in simple on/off mode.

Schematic

Code snippet

const int LDR_PIN = A0;      // Analog input
const int BUTTON_PIN = 2;    // Digital input
const int PWM_LED_PIN = 9;   // PWM output
const int DIGITAL_LED_PIN = 13; // Digital output

int lastButtonState = HIGH;
bool ledState = false;

void setup() {
  pinMode(BUTTON_PIN, INPUT);
  pinMode(PWM_LED_PIN, OUTPUT);
  pinMode(DIGITAL_LED_PIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  // Read analog sensor
  int lightLevel = analogRead(LDR_PIN);
  
  // Convert 0-1023 range to 255-0 PWM range (inverted)
  int brightness = map(lightLevel, 0, 1023, 255, 0);
  analogWrite(PWM_LED_PIN, brightness);
  
  // Handle digital sensor
  int buttonState = digitalRead(BUTTON_PIN);
  if (buttonState != lastButtonState && buttonState == LOW) {
    ledState = !ledState;
    digitalWrite(DIGITAL_LED_PIN, ledState);
  }
  lastButtonState = buttonState;
  
  delay(50);
}

Demo

Analog Control:
The LDR continuously measures ambient light levels, producing variable voltage outputs that are converted to digital values through the Arduino’s ADC. This analog signal controls the first LED’s brightness through PWM, creating a smooth dimming effect as environmental lighting changes.

Digital Control:
The push button provides binary input (HIGH/LOW), demonstrating the discrete nature of digital sensors. Each button press toggles the second LED between two states, showing the fundamental difference between analog and digital control systems.

Reflection and Future Improvements
This project effectively demonstrates the distinction between analog and digital sensing and control. The analog sensor provides continuous, proportional control, while the digital sensor offers precise, binary control.

Potential improvements could include:

  • Adding hysteresis to the LDR readings to prevent flickering in borderline lighting conditions
  • Implementing an exponential mapping for the PWM values to create more natural-feeling brightness transitions
  • Adding a mode selector that allows the digital button to switch between different lighting patterns
  • Incorporating a digital temperature sensor to adjust brightness based on both light and temperature

These enhancements would create a more sophisticated adaptive lighting system while maintaining the fundamental demonstration of analog versus digital sensing and control.

Reading Reflection – Week 9

A new angle on creating meaningful interactions is provided by Tom Igoe’s observations on interactive art and physical computing. One crucial takeaway is his recommendation to let individuals to participate in a project in their own way rather than directing every detail. It can be tempting to give directions or explanations in interactive art, but Igoe contends that doing so can restrict the audience’s creativity. This method emphasizes how crucial it is to give consumers room to explore and interpret on their own terms, which will make the experience more memorable and intimate.

I became aware of the importance of basic, intuitive actions in design after seeing Igoe’s examples of ordinary gestures—such as tapping or moving through a space—used as interactive features. People can interact with technology naturally when these well-known motions are turned into interesting experiences. A project that combines commonplace activities with artistic involvement, such as one in which a person’s movement or touch activates music or graphics, seems both familiar and unexpected. It helps me consider how I may use such movements in my projects to produce interactions that seem natural and grab viewers’ interest.

My comprehension of user-centered design is further enhanced by his analogy between creating interactive art and directing a play. A skilled director creates the scene yet lets the actor interpret and react freely, not controlling every step. Similarly, creating a project that allows for user exploration changes the emphasis from the designer’s intention to the user’s experience, making every interaction special. In the future, I hope to develop designs that lead users through subliminal clues, empowering them to come to their own conclusions and derive personal meaning, transforming the encounter into a cooperative dialogue.