Skip to content

16. Interface and application programming

Group Assignment:

My Contribution: Lightbulb Toggle Walkthrough

For the group assignment, I implemented a Lightbulb Toggle system using Arduino and Python with PyQt5. This project demonstrated bidirectionla communication between a desktop GUI and an embedded board.

What I learned:

  1. Serial Communication: Understanding how to maintain state synchronization without creating infinite loops. When the GUI toggles the LED, it sends a command. When the physical button on D7 is pressed, the Arduino sends a status update back. Neither side echoes the command back, preventing state loops.

  2. State Management: Handling connection states gracefully - disabling controls when no port is open, updating UI colors to reflect connection status, and cleaning up resources properly on close.

This experience gave me a foundation in creating desktop interfaces for embedded systems, which directly influenced my individual assignment where I built a Python GUI to visualize NeoPixel colors.

Group Assignment Page


Individual Assignment: Potentiometer-Controlled NeoPixel Color Control

Overview

For my individual assignment, I created an interactive system that uses a potentiometer (input device) to control the color of NeoPixel LEDs (output device). I explored four progressive approaches:

Stage 1: Reading analog input and displaying values in Serial Monitor

Stage 2: Mapping analog values to specific RGB colors

Stage 3: Using HSV color space for smooth continuous color transitions

Stage 4: Python GUI visualizer that mirrors the NeoPixel colors

This project demonstrates input sensing, analog-to-digital conversion, and output control using the Seeed XIAO RP2040.


Hardware Setup

Component XIAO Pin GPIO Function
Potentiometer A1 GPIO27 Analog input for color control
NeoPixel Strip D10 GPIO12 RGB LED output

Wiring Diagram:

Potentiometer:
  Left pin  → 3.3V
  Middle pin → A1 (GPIO27)
  Right pin → GND

NeoPixel Strip:
  VCC  → 5V
  GND  → GND
  DIN  → D10 (GPIO12)

Stage 1: AnalogReadSerial - Reading the Potentiometer

Objective: Understand how to read analog values from the potentiometer and verify the input range.

At first, I used example from Arduino to understand how the potentiometer works.

Then I try to print value after mapping.

/*
  AnalogReadSerial - Basic analog input reading
  Reads potentiometer on A1 and prints values to Serial Monitor
*/

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

void loop() {
  // Read analog value from potentiometer (0-1023)
  int sensorValue = analogRead(A1);

  // Map raw value to 0-10 range for easier interpretation
  int val = map(sensorValue, 0, 1023, 0, 10);

  // Print to Serial Monitor
  Serial.println(val);

  delay(1);  // Small delay for stability
}

Results: - Rotating the potentiometer from minimum to maximum produced values from 0 to 10 - Serial Monitor displayed real-time values as the knob was turned - Verified that the analog input was working correctly before proceeding

Analog Read Result video:

 

Stage 2: RGB Control - Discrete Color Selection

Objective: Map analog values to specific RGB colors for a discrete color-changing effect.

In this part, I prompt AI to code for me for the basic understand. The code provided by AI did not work at first. I had to check and fix pins, and code for lighting the pixels up. I have added some descriptions in the code in order to understand what is running.

With this stage, I need an extra function setNeoPixelColor() to pick the color for neopixels from 0-10 value range. It took lots of time to find and pick the colors.

#include <Adafruit_NeoPixel.h>

// NeoPixel configuration
#define NEOPIXEL_PIN D10      // NEOPIXELS at D10
#define NUM_PIXELS 5          // Numbers of Neopixels

Adafruit_NeoPixel pixel(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  // Initialize the serial monitor with 9600 baud
  Serial.begin(9600);

  // Initialize the Neopixels
  pixel.begin();
  pixel.setBrightness(50);    // (0-255) brightness of each pixel
  pixel.show();
}

void loop() {
  // Read analog input on A1
  int sensorValue = analogRead(A1);

  // Map 0-1023 to 0-10 range (11 discrete colors)
  int val = map(sensorValue, 0, 1023, 0, 10);

  // Print for debugging
  Serial.print("Sensor: ");
  Serial.print(sensorValue);
  Serial.print(" | Mapped: ");
  Serial.print(val);
  Serial.print(" | Color: ");

  // Set color based on mapped value
  setNeoPixelColor(val);

  delay(50);
}

void setNeoPixelColor(int value) {
  uint32_t color;

  switch(value) {
    case 0:
      color = pixel.Color(255, 0, 0);      // Red
      Serial.println("RED");
      break;
    case 1:
      color = pixel.Color(255, 50, 0);     // Orange-red
      Serial.println("ORANGE-RED");
      break;
    case 2:
      color = pixel.Color(255, 100, 0);    // Orange
      Serial.println("ORANGE");
      break;
    case 3:
      color = pixel.Color(255, 150, 0);    // Yellow-orange
      Serial.println("YELLOW-ORANGE");
      break;
    case 4:
      color = pixel.Color(255, 200, 0);    // Yellow
      Serial.println("YELLOW");
      break;
    case 5:
      color = pixel.Color(0, 255, 0);      // Green
      Serial.println("GREEN");
      break;
    case 6:
      color = pixel.Color(0, 255, 100);    // Light green
      Serial.println("LIGHT GREEN");
      break;
    case 7:
      color = pixel.Color(0, 200, 255);    // Cyan
      Serial.println("CYAN");
      break;
    case 8:
      color = pixel.Color(0, 100, 255);    // Light blue
      Serial.println("LIGHT BLUE");
      break;
    case 9:
      color = pixel.Color(0, 0, 255);      // Blue
      Serial.println("BLUE");
      break;
    case 10:
      color = pixel.Color(255, 0, 255);    // Purple
      Serial.println("PURPLE");
      break;
    default:
      color = pixel.Color(0, 0, 0);        // Off
      Serial.println("OFF");
      break;
  }

  // Apply color to all NeoPixels
  for(uint16_t i = 0; i < NUM_PIXELS; i++) {
    pixel.setPixelColor(i, color);
  }
  pixel.show();
}

Results:

  • As the potentiometer was rotated, the NeoPixels cycled through 11 distinct colors
  • Each color change was clearly visible and corresponded to the mapped value range
  • Serial Monitor confirmed the color name for each position
Value Color RGB Value
0 Red (255,0,0)
2 Orange (255,100,0)
4 Yellow (255,200,0)
5 Green (0,255,0)
7 Cyan (0,200,255)
9 Blue (0,0,255)
10 Purple (255,0,255)

RGB Potentiometer result video

 

Stage 3: HSV Control - Smooth Continuous Color Transition

Objective: Use HSV (Hue, Saturation, Value) color space for seamless, smooth color transitions across the full spectrum.

Color spectrum

I got this idea from Toni - Fab Lab Oulu staff. Due to the distinction from RGB, I and Toni came up with a smoother transition based on HSV.

With this, I changed the map 0-65536 range correspond to a full circle of color. However I did not want to repeat the RED so I took 65536 x 7/8

After this idea, I can remove the extra function and shorten the code.

Shorter Loop code

In this stage, I have not coded the color part. If you want to show color on the Serial Monitor. You also need an extra function to work on the val parameter.

#include <Adafruit_NeoPixel.h>

// NeoPixel configuration
#define NEOPIXEL_PIN D10      // GPIO12
#define NUM_PIXELS 5          // 5 NeoPixels

Adafruit_NeoPixel pixel(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

void setup() {
  // Initialize the serial monitor with 9600 baud
  Serial.begin(9600);

  // Initialize the neopixels
  pixel.begin();
  pixel.setBrightness(50);    // brightness (0-255)
  pixel.show();
}

void loop() {
  // Read analog input on A1
  int sensorValue = analogRead(A1);

  // Map 0-1023 to full HSV hue range (0 to 57344)
  // 65536 * 7/8 = 57344 (max hue value for ColorHSV function)
  int val = map(sensorValue, 0, 1023, 0, 65536 * 7 / 8);

  // Print for debugging
  Serial.print("Sensor: ");
  Serial.print(sensorValue);
  Serial.print(" | Value: ");
  Serial.println(val);

  // Create color using HSV (full saturation, full brightness)
  uint32_t color = pixel.ColorHSV(val);

  // Apply color to all NeoPixels
  for(uint16_t i = 0; i < NUM_PIXELS; i++) {
    pixel.setPixelColor(i, color);
  }
  pixel.show();

  delay(50);
}

HSV Color Wheel:

val 0      → Red
val 2466   → Orange
val 8127  → Yellow
val 21637  → Green
val 32768  → Cyan
val 43691  → Blue
val 49552  → Purple
val 57344  → Magenta

Results:

  • The NeoPixels now transition smoothly through the entire color spectrum
  • No discrete jumps - continuous color change as the knob rotates
  • All LEDs display the same color simultaneously
  • The effect is visually pleasing and demonstrates the full range of the NeoPixel

HVS result video

 

Because the bad connection of junping wires, so I soldered the male pins of the jumping wires into potentiometer legs for better data. However, there are sometimes noise from the connections.


Stage 4: Python GUI Visualizer

Objective: Create a desktop application that reads the Arduino’s serial output and displays the current color in real-time, matching the NeoPixel colors

After learning from my group assignment with PyQt5, I use AI to generate a python file based on lightbulb_toggle.py.

How It Work:

After the file is generated, I dived into the file to understand it.

The Arduino sends data over serial in the format:

Sensor: 512 | Value: 28672

The python application:

  1. Connects to the Arduino via serial port
  2. Parses the incoming data to extract the sensor value and hue value
  3. Converts the hue value to RGB using the same HSV-to-RGB formula as the NeoPixel library
  4. Updates the window background color to match the NeoPixel
  5. Displays sensor value, hue value, and RGB components

How the python inteface works:

Component Function
ColorDisplayWidget Large area showing the current color
ValueDisplayWidget Show sensor value, hue, and RGB value
Serial Polling QTimer checks for new data every 20ms
Data Parsing Extracts “Sendor:” and “Value:” from serial
HSV to RGB Converts hue to RGB using same formula as NeoPixel

Code:

#!/usr/bin/env python3
"""
HSV NeoPixel Color Visualizer
By Fab Academy Student

This interface connects to the Arduino running HSV_potentialmeter.ino,
reads the sensor values, and displays the corresponding color in the window.
The background color matches the NeoPixel colors in real-time.

Requires
--------
    pip install pyserial pyqt5
"""

import sys
from typing import Optional

import serial
import serial.tools.list_ports

from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QComboBox,
    QSizePolicy,
    QFrame,
)
from PyQt5.QtCore import (
    Qt,
    QTimer,
    QRect,
    pyqtSignal,
)
from PyQt5.QtGui import (
    QPainter,
    QColor,
    QFont,
    QBrush,
    QLinearGradient,
    QPalette,
    QPixmap,
)


# =============================================================================
# CONSTANTS
# =============================================================================

BAUD_RATE = 9600  # Must match the Arduino sketch

# Color palette
COL_BG_DARK = QColor(20, 20, 28)
COL_PANEL_BG = QColor(35, 35, 45)
COL_TEXT = QColor(220, 220, 230)
COL_TEXT_DIM = QColor(150, 150, 165)
COL_BORDER = QColor(80, 80, 95)
COL_VALUE_HIGHLIGHT = QColor(100, 220, 130)


# =============================================================================
# COLOR WIDGET
# =============================================================================

class ColorDisplayWidget(QWidget):
    """
    Custom widget that displays a solid color background.
    Updates its color when new HSV values are received.
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.current_color = QColor(50, 50, 60)  # Default dark gray
        self.setMinimumSize(400, 300)
        self.setAutoFillBackground(True)

    def update_color(self, r: int, g: int, b: int) -> None:
        """Update the displayed color and refresh the widget."""
        self.current_color = QColor(r, g, b)
        self.update()  # Schedule repaint

    def paintEvent(self, event) -> None:
        """Paint the widget with the current color."""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        # Fill the entire widget with the current color
        painter.fillRect(self.rect(), self.current_color)

        # Draw a subtle inner glow effect
        gradient = QLinearGradient(0, 0, 0, self.height())
        gradient.setColorAt(0.0, QColor(255, 255, 255, 30))
        gradient.setColorAt(0.5, QColor(255, 255, 255, 0))
        gradient.setColorAt(1.0, QColor(0, 0, 0, 30))
        painter.fillRect(self.rect(), gradient)

        painter.end()


# =============================================================================
# VALUE DISPLAY WIDGET
# =============================================================================

class ValueDisplayWidget(QFrame):
    """
    Widget that displays sensor readings and RGB values in a clean card layout.
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet(self._card_style())
        self.setFixedHeight(120)

        self.sensor_value = 0
        self.hue_value = 0
        self.rgb_values = (0, 0, 0)

        self._build_ui()

    def _build_ui(self):
        """Create the layout for value displays."""
        layout = QHBoxLayout(self)
        layout.setContentsMargins(20, 15, 20, 15)
        layout.setSpacing(30)

        # Sensor Value Display
        sensor_widget = QWidget()
        sensor_layout = QVBoxLayout(sensor_widget)
        sensor_layout.setSpacing(5)

        sensor_label = QLabel("📊 SENSOR VALUE")
        sensor_label.setStyleSheet("color: rgb(150, 150, 165); font-size: 11px; font-weight: bold;")
        sensor_layout.addWidget(sensor_label)

        self.sensor_display = QLabel("0")
        self.sensor_display.setStyleSheet("color: rgb(100, 220, 130); font-size: 32px; font-weight: bold; font-family: monospace;")
        sensor_layout.addWidget(self.sensor_display)

        layout.addWidget(sensor_widget)

        # Separator line
        line = QFrame()
        line.setFrameShape(QFrame.VLine)
        line.setStyleSheet("background-color: rgb(80, 80, 95);")
        line.setFixedWidth(2)
        layout.addWidget(line)

        # Hue Value Display
        hue_widget = QWidget()
        hue_layout = QVBoxLayout(hue_widget)
        hue_layout.setSpacing(5)

        hue_label = QLabel("🎨 HUE VALUE")
        hue_label.setStyleSheet("color: rgb(150, 150, 165); font-size: 11px; font-weight: bold;")
        hue_layout.addWidget(hue_label)

        self.hue_display = QLabel("0")
        self.hue_display.setStyleSheet("color: rgb(220, 220, 230); font-size: 32px; font-weight: bold; font-family: monospace;")
        hue_layout.addWidget(self.hue_display)

        layout.addWidget(hue_widget)

        # Separator line
        line2 = QFrame()
        line2.setFrameShape(QFrame.VLine)
        line2.setStyleSheet("background-color: rgb(80, 80, 95);")
        line2.setFixedWidth(2)
        layout.addWidget(line2)

        # RGB Value Display
        rgb_widget = QWidget()
        rgb_layout = QVBoxLayout(rgb_widget)
        rgb_layout.setSpacing(5)

        rgb_label = QLabel("🌈 RGB VALUES")
        rgb_label.setStyleSheet("color: rgb(150, 150, 165); font-size: 11px; font-weight: bold;")
        rgb_layout.addWidget(rgb_label)

        rgb_value_layout = QHBoxLayout()
        rgb_value_layout.setSpacing(8)

        self.r_display = QLabel("R: 0")
        self.r_display.setStyleSheet("color: rgb(255, 100, 100); font-size: 18px; font-weight: bold;")
        self.g_display = QLabel("G: 0")
        self.g_display.setStyleSheet("color: rgb(100, 255, 100); font-size: 18px; font-weight: bold;")
        self.b_display = QLabel("B: 0")
        self.b_display.setStyleSheet("color: rgb(100, 100, 255); font-size: 18px; font-weight: bold;")

        rgb_value_layout.addWidget(self.r_display)
        rgb_value_layout.addWidget(self.g_display)
        rgb_value_layout.addWidget(self.b_display)
        rgb_layout.addLayout(rgb_value_layout)

        layout.addWidget(rgb_widget)

    def update_values(self, sensor: int, hue: int, rgb: tuple) -> None:
        """Update all value displays."""
        self.sensor_value = sensor
        self.hue_value = hue
        self.rgb_values = rgb

        self.sensor_display.setText(str(sensor))
        self.hue_display.setText(str(hue))
        self.r_display.setText(f"R: {rgb[0]}")
        self.g_display.setText(f"G: {rgb[1]}")
        self.b_display.setText(f"B: {rgb[2]}")

    @staticmethod
    def _card_style() -> str:
        return """
            QFrame {
                background-color: rgb(35, 35, 45);
                border-radius: 12px;
                border: 1px solid rgb(80, 80, 95);
            }
        """


# =============================================================================
# COLOR PREVIEW WIDGET
# =============================================================================

class ColorPreviewWidget(QFrame):
    """
    Widget that shows a small color swatch preview of the current color.
    """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFixedSize(80, 80)
        self.current_color = QColor(50, 50, 60)
        self.setStyleSheet(self._preview_style())

    def _preview_style(self) -> str:
        return """
            QFrame {
                border-radius: 12px;
                border: 2px solid rgb(100, 100, 120);
            }
        """

    def update_color(self, r: int, g: int, b: int) -> None:
        """Update the preview color."""
        self.current_color = QColor(r, g, b)
        self.setStyleSheet(f"""
            QFrame {{
                background-color: rgb({r}, {g}, {b});
                border-radius: 12px;
                border: 2px solid rgb(100, 100, 120);
            }}
        """)

    def paintEvent(self, event) -> None:
        """Paint the preview with the current color."""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.fillRect(self.rect(), self.current_color)
        painter.end()


# =============================================================================
# MAIN WINDOW
# =============================================================================

class MainWindow(QMainWindow):
    """
    Main application window that connects to Arduino and displays the color.
    """

    def __init__(self):
        super().__init__()
        self.setWindowTitle("HSV NeoPixel Color Visualizer")
        self.setMinimumSize(500, 500)

        self.serial_port: Optional[serial.Serial] = None
        self.current_hue = 0
        self.current_sensor = 0

        self._build_ui()
        self._refresh_ports()

        # Timer for polling serial data
        self.poll_timer = QTimer(self)
        self.poll_timer.timeout.connect(self._poll_serial)
        self.poll_timer.start(20)  # 50 times per second

    def _build_ui(self) -> None:
        """Build the user interface."""
        root = QWidget()
        root.setStyleSheet("background-color: rgb(20, 20, 28);")
        self.setCentralWidget(root)

        main_layout = QVBoxLayout(root)
        main_layout.setContentsMargins(20, 20, 20, 20)
        main_layout.setSpacing(15)

        # ---- Header Section ----
        header_layout = QHBoxLayout()

        # Title
        title_label = QLabel("🎨 HSV NeoPixel Visualizer")
        title_label.setStyleSheet("color: rgb(220, 220, 230); font-size: 20px; font-weight: bold;")
        header_layout.addWidget(title_label)

        header_layout.addStretch()

        # Color Preview
        self.preview = ColorPreviewWidget()
        header_layout.addWidget(self.preview)

        main_layout.addLayout(header_layout)

        # ---- Subtitle ----
        subtitle = QLabel("Real-time color matching with NeoPixel")
        subtitle.setStyleSheet("color: rgb(150, 150, 165); font-size: 12px;")
        main_layout.addWidget(subtitle)

        # ---- Main Color Display ----
        self.color_display = ColorDisplayWidget()
        self.color_display.setMinimumHeight(350)
        main_layout.addWidget(self.color_display)

        # ---- Value Display Panel ----
        self.value_display = ValueDisplayWidget()
        main_layout.addWidget(self.value_display)

        # ---- Serial Control Panel ----
        control_panel = QFrame()
        control_panel.setStyleSheet(self._panel_style())
        control_layout = QHBoxLayout(control_panel)
        control_layout.setContentsMargins(15, 10, 15, 10)

        # Port selector
        port_label = QLabel("Serial Port:")
        port_label.setStyleSheet("color: rgb(220, 220, 230);")
        control_layout.addWidget(port_label)

        self.port_combo = QComboBox()
        self.port_combo.setStyleSheet(self._combo_style())
        self.port_combo.setMinimumWidth(150)
        control_layout.addWidget(self.port_combo)

        # Buttons
        self.refresh_btn = self._make_btn("Refresh", self._refresh_ports)
        self.open_btn = self._make_btn("Open", self._open_port)
        self.close_btn = self._make_btn("Close", self._close_port)

        control_layout.addWidget(self.refresh_btn)
        control_layout.addWidget(self.open_btn)
        control_layout.addWidget(self.close_btn)

        control_layout.addStretch()

        # Status indicator
        self.status_label = QLabel("● Disconnected")
        self.status_label.setStyleSheet("color: rgb(200, 80, 80); font-weight: bold;")
        control_layout.addWidget(self.status_label)

        main_layout.addWidget(control_panel)

        # ---- Instruction Label ----
        instruction = QLabel("💡 Connect to Arduino and rotate the potentiometer to see the color change")
        instruction.setStyleSheet("color: rgb(100, 100, 110); font-size: 11px;")
        instruction.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(instruction)

        self.setFixedSize(600, 680)

    def _make_btn(self, label: str, slot) -> QPushButton:
        """Create a styled button."""
        btn = QPushButton(label)
        btn.setFixedHeight(32)
        btn.setStyleSheet(self._btn_style())
        btn.clicked.connect(slot)
        return btn

    # ------------------------------------------------------------- Serial --

    def _refresh_ports(self) -> None:
        """Refresh the list of available serial ports."""
        self.port_combo.clear()
        ports = sorted(serial.tools.list_ports.comports(), key=lambda p: p.device)
        for p in ports:
            self.port_combo.addItem(p.device)
        if self.port_combo.count() == 0:
            self.port_combo.addItem("No ports found")

    def _open_port(self) -> None:
        """Open the selected serial port."""
        name = self.port_combo.currentText()
        if not name or name == "No ports found":
            return
        try:
            self.serial_port = serial.Serial(name, BAUD_RATE, timeout=0)
            self.status_label.setText("● Connected")
            self.status_label.setStyleSheet("color: rgb(100, 220, 130); font-weight: bold;")
            self.port_combo.setStyleSheet(self._combo_style(highlight=True))
            print(f"Opened: {name}")
        except serial.SerialException as e:
            print(f"Could not open {name}: {e}")
            self.status_label.setText("● Connection Failed")
            self.status_label.setStyleSheet("color: rgb(200, 80, 80); font-weight: bold;")

    def _close_port(self) -> None:
        """Close the serial port."""
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        self.serial_port = None
        self.status_label.setText("● Disconnected")
        self.status_label.setStyleSheet("color: rgb(200, 80, 80); font-weight: bold;")
        self.port_combo.setStyleSheet(self._combo_style())
        print("Port closed.")

    def _poll_serial(self) -> None:
        """
        Poll the serial port for incoming data.
        The Arduino sends: "Sensor: [value] | Mapped: [hue]"
        We need to parse these lines to extract the values.
        """
        if not self.serial_port or not self.serial_port.is_open:
            return

        try:
            while self.serial_port.in_waiting:
                raw = self.serial_port.readline()
                line = raw.decode("utf-8", errors="ignore").strip()

                if line:
                    self._parse_message(line)

        except serial.SerialException:
            self._close_port()

    def _parse_message(self, line: str) -> None:
        """
        Parse the serial message from Arduino.
        Expected format: "Sensor: 512 | Mapped: 28672"
        """
        try:
            # Look for "Sensor:" and "Mapped:" patterns
            if "Sensor:" in line and "Mapped:" in line:
                parts = line.split("|")

                # Extract sensor value
                sensor_part = parts[0].split("Sensor:")[1].strip()
                sensor_value = int(sensor_part)

                # Extract mapped value (hue)
                mapped_part = parts[1].split("Mapped:")[1].strip()
                hue_value = int(mapped_part)

                self.current_sensor = sensor_value
                self.current_hue = hue_value

                # Convert hue to RGB using the same formula as NeoPixel
                rgb = self._hue_to_rgb(hue_value)

                # Update the display
                self.color_display.update_color(rgb[0], rgb[1], rgb[2])
                self.preview.update_color(rgb[0], rgb[1], rgb[2])
                self.value_display.update_values(sensor_value, hue_value, rgb)

        except (ValueError, IndexError) as e:
            # Silently ignore parsing errors
            pass

    def _hue_to_rgb(self, hue: int) -> tuple:
        """
        Convert hue value (0-57344) to RGB.
        This mimics the NeoPixel ColorHSV conversion.
        """
        # Normalize hue to 0-360 degrees
        hue_deg = (hue / 57344.0) * 360.0

        # Convert HSV to RGB (full saturation and value for max brightness)
        h = hue_deg / 60.0
        i = int(h)
        f = h - i

        p = 0
        q = int(255 * (1 - f))
        t = int(255 * f)

        if i == 0:
            r, g, b = 255, t, p
        elif i == 1:
            r, g, b = q, 255, p
        elif i == 2:
            r, g, b = p, 255, t
        elif i == 3:
            r, g, b = p, q, 255
        elif i == 4:
            r, g, b = t, p, 255
        else:
            r, g, b = 255, p, q

        return (r, g, b)

    # ------------------------------------------------------------ Styles --

    @staticmethod
    def _btn_style() -> str:
        return """
            QPushButton {
                background-color: rgb(65, 65, 78);
                color: rgb(210, 210, 210);
                border: 1px solid rgb(110, 110, 110);
                border-radius: 5px;
                padding: 0 12px;
                font-size: 12px;
            }
            QPushButton:hover {
                background-color: rgb(80, 80, 95);
            }
            QPushButton:pressed {
                background-color: rgb(50, 50, 62);
            }
        """

    @staticmethod
    def _combo_style(highlight: bool = False) -> str:
        border = "rgb(100, 220, 130)" if highlight else "rgb(90, 90, 90)"
        text = "rgb(100, 220, 130)" if highlight else "rgb(210, 210, 210)"
        return f"""
            QComboBox {{
                background-color: rgb(45, 45, 52);
                color: {text};
                border: 1px solid {border};
                border-radius: 4px;
                padding: 4px 8px;
                font-size: 12px;
            }}
            QComboBox::drop-down {{ border: none; }}
            QComboBox QAbstractItemView {{
                background-color: rgb(45, 45, 52);
                color: rgb(200, 200, 200);
                selection-background-color: rgb(65, 65, 78);
            }}
        """

    @staticmethod
    def _panel_style() -> str:
        return """
            QFrame {
                background-color: rgb(28, 28, 35);
                border-radius: 8px;
                border: 1px solid rgb(60, 60, 70);
            }
        """

    def closeEvent(self, event) -> None:
        """Clean up before closing."""
        self._close_port()
        self.poll_timer.stop()
        event.accept()


# =============================================================================
# ENTRY POINT
# =============================================================================

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

Result:

The python interface succesfully:

  • Connects to the Arduino via serial port
  • Reads and parses the HSV values in real-time
  • Updates the window background to match the NeoPixel color
  • Displays sensor and color information
  • Provides a clean, professionla desktop interface

This extension demonstrates how embedded systems can be combined with desktop applications to create rich user experiences.

Before Connection

Before connection

Visualization video:

 

Comparison: RGB vs HSV Approach

Aspect RGB (Discrete) HSV (Continuous)
Color Transitions 11 discrete colors Continuous spectrum
Code Complexity Switch statement, manual RGB values Single ColorHSV function
Smoothness Jumps between colors Seamless transitions
Memory Usage Larger code More compact code
User Experience Preset colors only Full color control

Testing and Results

Test Stage Input Expected Output Actual Result
Stage 1 Rotate potentiometer Values 0-10 in Serial Monitor ✓ Passed
Stage 2 Rotate potentiometer 11 discrete colors on NeoPixels ✓ Passed
Stage 3 Rotate potentiometer Continuous color spectrum ✓ Passed
Stage 4 Rotate potentiometer Python window color matches NeoPixel ✓ Passed

Observations:

  1. Analog Resolution: The 10-bit ADC (0-1023) provided sufficient resolution for smooth control
  2. NeoPixel Response: Immediate response to input changes with no noticeable lag
  3. HSV Advantage: Much smoother transitions compared to discrete RGB mapping
  4. Python Interface: Real-time color visualization with less than 50ms latency

Troubleshooting:

Issue: Built-in NeoPixel not lighting up initially

  • Solution: Try to use external NeoPixel, then try again with built-in one and figured about the wrong pins

Issue: HSV colors were not matching expected spectrum

  • Solution: Adjusted the mapping range to 65536 * 7/8 (57344) for full hue rotation

Issue: Python not receiving serial data

  • Solution: Ensured baud rate matched (9600) and used timeout=0 for non-blocking reads

Reflection

This assignment provided valuable experience in:

  1. Analog Input: Understanding how to read and map analog signals from a potentiometer
  2. NeoPixel Library: Learning the Adafruit_NeoPixel library for RGB LED control
  3. Color Theory: Exploring the difference between RGB and HSV color spaces
  4. Progressive Development: Building complexity incrementally (reading → discrete colors → continuous colors)
  5. Serial Debugging: Using Serial Monitor to verify input values during development
  6. Application Programming: Creating a Python GUI with PyQt5 to visualize embedded data

Key Takeaway: The HSV approach is significantly more efficient and produces better visual results for continuous color control compared to manually mapping RGB values. The ColorHSV() function handles the complex color conversion internally, making the code cleaner and more maintainable.


Files

File Name Description
AnalogReadSerial.ino Basic analog input reading example
RGB_potentialmeter.ino Discrete RGB color control with potentiometer
HSV_potentialmeter.ino Continuous HSV color control with potentiometer
HSV_visualize.py Python PyQt5 interface for color visualization