Interface and Application Programming
In many embedded system projects, it's necessary to establish a reliable and intuitive way for users to interact with the hardware. While command-line tools and serial terminals offer basic communication, they are rarely suitable for end users or for practical deployment. This is where user interfaces become essential: they serve as the communication bridge between the human operator and the system's internal logic.
A user interface (UI) can take many forms, from simple physical buttons and displays to complex graphical applications or web-based dashboards. The goal is to facilitate control and monitoring in a way that is both effective and user-friendly. A well-designed interface not only improves usability, but also enhances data interpretation and system reliability through visual or interactive feedback.

Figure 1: Overview of physical, graphical, and command-based interfaces
Types of Interfaces
Interfaces can be broadly categorized based on how interaction is achieved. Each type has advantages and limitations, and their suitability depends on the system's complexity, required feedback, and context of use. Below is a comparative summary:
Type | Description | Advantages | Limitations |
---|---|---|---|
Physical Interfaces | Include push buttons, rotary encoders, knobs, or touchpads directly wired to a microcontroller. | Immediate response, simple implementation, does not require software drivers. | Low scalability, limited to basic interaction. |
Command-Line Interfaces (CLI) | Interaction is performed through textual commands via a serial monitor or terminal. | Precise control, efficient for debugging and development. | Not user-friendly, no graphical feedback. |
Graphical User Interfaces (GUI) | Desktop applications with buttons, sliders, input fields, etc. | High flexibility, intuitive control, supports real-time feedback. | Requires software libraries and more processing resources. |
Web Interfaces | Built using HTML, CSS, and JavaScript, hosted locally or remotely. | Cross-platform access, remote control capability. | Requires networking knowledge, setup is more complex. |

Figure 2: Use-case suitability of various interfaces
Frameworks for Interface Development
Several programming frameworks exist to build user interfaces, each with different features and ideal use cases. The choice depends on the hardware platform, the communication protocol, and the complexity of interaction needed.
Framework | Language | Best Use Case | Remarks |
---|---|---|---|
PyQt / PySide | Python | Desktop GUI apps for local control and visualization | Extensive documentation, supports complex layouts and signals/slots |
MQTT | Any (Python, C++, etc.) | Publish/subscribe messaging between devices, ideal for IoT | Lightweight protocol, requires a broker (e.g., Mosquitto, EMQX) |
Tkinter | Python | Simple GUIs with minimal dependencies | Included in Python standard library, limited aesthetics |
Flask / Django + JS | Python + HTML/CSS/JS | Web-based dashboards and control panels | Supports REST APIs, asynchronous updates with JavaScript |

Figure 3: Example architectures for PyQt, MQTT, and Web-based interfaces
Why PyQt Was Selected
For this particular project, PyQt was chosen due to its robust support for graphical user interfaces and its seamless integration with Python. PyQt is a set of Python bindings for the Qt toolkit, which is known for its stability and flexibility across platforms (and also because it's the easiest in my opinion xD).
The PyQt framework enables the creation of fully-featured desktop applications with components such as buttons, sliders, labels, and input fields. One of its most valuable features is the signal-slot mechanism, which allows actions (like button clicks) to trigger specific responses in the code. PyQt also handles window management, widget layout, event handling, and even styling (via Qt Stylesheets), allowing for a professional interface without needing low-level graphical programming.

Figure 4: Example interface developed using PyQt5
Overview of Python Libraries Used
The development of the interface involved multiple Python libraries, each responsible for a different aspect of the application. Below is a brief summary:
- PyQt5: Provides all GUI components and handles the event-driven architecture of the interface.
- PySerial: Manages the serial communication between the computer and the microcontroller via USB. It enables sending commands and receiving sensor data.
- Threading: Ensures that background serial reading does not freeze the GUI. It allows parallel tasks while keeping the interface responsive.
- Math: Used for numerical operations, especially for interpreting sensor readings (e.g., computing angles from accelerometer data).
- Pygame + OpenGL: Optional modules used for rendering 3D visualizations of motion data in real time. They help in representing pitch, roll, or yaw.
This combination of libraries enables a dynamic interface capable of reading sensor input, sending actuator commands, and visualizing feedback without interrupting user interaction.
Microcontroller and Communication Overview
Now, I will briefly examine how the microcontrollers code handles communication and hardware control, before moving into a block-by-block explanation of the GUI code structure.
The code is in C++ compiled in the Arduino IDE and is very simple, it listens to serial data and controls a NeoPixel LED and a servo motor accordingly. It also sends back sensor readings (from the MPU6050) that the GUI can interpret and display.
// Include required libraries for the components used #include <Adafruit_NeoPixel.h> #include <Servo.h> #include <Wire.h> #include <Adafruit_MPU6050.h> #include <Adafruit_Sensor.h> // Define NeoPixel pin and power pin #define NEOPIXEL_PIN 12 #define NEO_PWR_PIN 11 #define NUMPIXELS 1 // Create instances for LED strip, servo motor and sensor Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); Servo SM; Adafruit_MPU6050 mpu; // UART communication variables String Save_S = ""; String Sub_1 = ""; char Det = ' '; char Decide; int I_end; int I_start = 0; unsigned long lastReadTime = 0; const unsigned long readInterval = 20; // Struct to store incoming data from PC (RGB, Angle, Servo status) typedef struct { int Angle = 90; int R = 255; int G = 0; int B = 0; bool S = false; } XIAO_DATA_RECV; // Struct to store outgoing data from sensor (acceleration and gyro) typedef struct { int Acc_x = 0; int Acc_y = 0; int Acc_z = 0; int Gyro_x = 0; int Gyro_y = 0; int Gyro_z = 0; } XIAO_DATA_SENT; XIAO_DATA_RECV Data_esp; XIAO_DATA_SENT Data_hub; // Function to move to the next segment in the received string void NextString() { I_start = I_end + 1; Save_S = Save_S.substring(I_start); I_start = 0; I_end = Save_S.indexOf(Det); } void setup() { Serial.begin(115200); SM.attach(D0); pinMode(NEO_PWR_PIN, OUTPUT); digitalWrite(NEO_PWR_PIN, HIGH); pixels.begin(); pixels.show(); if (!mpu.begin()) { while (1) delay(10); } mpu.setAccelerometerRange(MPU6050_RANGE_8_G); mpu.setGyroRange(MPU6050_RANGE_500_DEG); mpu.setFilterBandwidth(MPU6050_BAND_21_HZ); delay(1000); } void loop() { // 1. Read sensor data unsigned long currentTime = millis(); if (currentTime - lastReadTime >= readInterval) { lastReadTime = currentTime; sensors_event_t a, g, temp; mpu.getEvent(&a, &g, &temp); Data_hub.Acc_x = (int)a.acceleration.x; Data_hub.Acc_y = (int)a.acceleration.y; Data_hub.Acc_z = (int)a.acceleration.z; Data_hub.Gyro_x = (int)g.gyro.x; Data_hub.Gyro_y = (int)g.gyro.y; Data_hub.Gyro_z = (int)g.gyro.z; Serial.print("Ax" + String(Data_hub.Acc_x) + Det); Serial.print("Ay" + String(Data_hub.Acc_y) + Det); Serial.print("Az" + String(Data_hub.Acc_z) + Det); Serial.print("Gx" + String(Data_hub.Gyro_x) + Det); Serial.print("Gy" + String(Data_hub.Gyro_y) + Det); Serial.println("Gz" + String(Data_hub.Gyro_z)); } // 2. Read incoming UART commands if (Serial.available() > 0) { Save_S = Serial.readStringUntil('\n') + Det; I_end = Save_S.indexOf(Det); while (I_end > 0) { Sub_1 = Save_S.substring(I_start, I_end); Decide = Sub_1.charAt(0); if (Decide == 'A') Data_esp.Angle = Sub_1.substring(1, I_end).toInt(); if (Decide == 'R') Data_esp.R = Sub_1.substring(1, I_end).toInt(); if (Decide == 'G') Data_esp.G = Sub_1.substring(1, I_end).toInt(); if (Decide == 'B') Data_esp.B = Sub_1.substring(1, I_end).toInt(); if (Decide == 'S') Data_esp.S = Sub_1.substring(1, I_end).toInt(); NextString(); } } // 3. Apply RGB and Servo angle pixels.setPixelColor(0, pixels.Color(Data_esp.R, Data_esp.G, Data_esp.B)); pixels.show(); if (Data_esp.S) SM.write(Data_esp.Angle); }
1. Included Libraries
The following libraries are included to support sensor reading, NeoPixel control, and servo operation:
- Adafruit_NeoPixel.h: Enables control of RGB LEDs with various color formats.
- Servo.h: Provides easy control for standard servos.
- Wire.h: Facilitates I2C communication, required by the MPU6050.
- Adafruit_MPU6050.h and Adafruit_Sensor.h: Used to initialize and read data from the MPU6050 (accelerometer + gyroscope).
2. Variable and Object Definitions
These are the core objects and variables declared to manage hardware and communication:
- pixels: Controls a single NeoPixel LED connected to pin 12.
- SM: Servo motor instance connected to pin D0.
- mpu: Sensor object to access accelerometer and gyroscope data.
- Save_S, >Sub_1: Used to temporarily store and process incoming serial strings.
- Data_esp: Struct that stores received values (angle, RGB, and a boolean flag for servo control).
- Data_hub: Struct that holds sensor readings to be sent back to the main controller.
3. Setup Function
The setup()
function initializes all hardware components:
- Begins serial communication at 115200 baud.
- Attaches the servo motor to its respective pin.
- Enables power to the NeoPixel and initializes its configuration.
- Initializes the MPU6050 sensor and sets its sensitivity and filtering parameters.
- Adds a small delay to ensure everything stabilizes.
4. Loop Function
The loop()
function performs two main tasks: sending sensor data and receiving control commands.
4.1 Sending Sensor Data
Every 20 milliseconds, the code checks if enough time has passed since the last reading. If so, it captures the accelerometer and gyroscope data from the MPU6050 and sends it via UART using a delimited string format.
Serial.print("Ax" + String(Data_hub.Acc_x) + Det);
Each data point is labeled (e.g., Ax
, Gx
) and followed by a delimiter character for easy parsing on the receiving side.
4.2 Receiving Serial Commands
The code listens for incoming strings terminated with a newline character. Once a complete message is received, it's processed using a while-loop and the function NextString()
to extract segments one by one.
- Each segment starts with a character (e.g.,
'R'
,'A'
,'S'
) indicating the data type. - The corresponding value is parsed and stored in
Data_esp
.
4.3 Acting on Received Data
Once the incoming values are processed:
- The RGB values are applied to the NeoPixel using
pixels.setPixelColor()
. - If the flag
Data_esp.S
is true, the servo is moved to the specified angle usingSM.write()
.
5. Auxiliary Function: NextString()
This small utility function is used to advance through the serial string buffer by identifying the next delimiter and updating indices accordingly. It allows efficient parsing of multiple values received in a single string (I also created this function because I initially thought I'd be repeatedly generating many substrings. Therefore, it would be easier to manage everything in one function).
void NextString() {
I_start = I_end + 1;
Save_S = Save_S.substring(I_start);
I_start = 0;
I_end = Save_S.indexOf(Det);
}


Figure 7: Example interface developed using PyQt5
PyQt5 GUI Fundamentals and Serial Communication
This section explains the foundational elements of the GUI built with PyQt5 used in the project, particularly focusing on how widgets are created and managed, as well as how serial communication is implemented in a non-blocking and responsive way.
Essential Libraries and Their Roles
Library | Purpose | Key Elements Used |
---|---|---|
PyQt5.QtWidgets |
Provides GUI components like windows, sliders, buttons, etc. |
|
PyQt5.QtCore |
Manages signals, threads, and application logic |
|
PyQt5.QtGui |
Handles visuals and styles |
|
serial / serial.tools.list_ports |
Detects available COM ports and manages UART communication |
|
Common PyQt Widgets Explained
The GUI consists of a mix of interactive widgets. Here's how they behave and interact in the application:
Widget | Description | Usage in App |
---|---|---|
QSlider |
A horizontal or vertical slider to choose values in a range | Used for selecting RGB values from 0 to 255 |
QDial |
Rotational input widget to select angular values | Controls the servo angle (0°–180°) |
QSpinBox |
Numeric input with up/down arrows | Synchronized with the dial for precise angle selection |
QPushButton |
Clickable button, optionally toggleable | Starts/stops servo and opens color picker |
QColorDialog |
Opens a window for color selection | Sets RGB sliders to selected color and updates preview |
QComboBox |
Dropdown list to choose between options | Selects the available COM port to open |
QLabel |
Displays static or dynamic text | Shows sensor data and status messages |
Serial Communication in PyQt
To avoid blocking the GUI while waiting for serial data, this application implements asynchronous serial communication using a QThread and a SerialWorker class:
- SerialWorker: Handles opening the port and reading data.
- run(): Continuously polls the port in a loop, emits a signal with new data.
- data_received signal: Transmits lines read from the port to the main thread safely.
The communication works as follows:
- When the user clicks Initialize, the COM port is opened with
serial.Serial(...)
. - A separate thread is created to run the reading loop using
QThread
. - Incoming lines (e.g.,
"Ax10 Ay0 Az20 Gx5 Gy3 Gz7"
) are emitted to the main GUI thread. - The GUI parses the data, updates the corresponding labels, and recalculates orientation values (covered later).
Sending data is synchronous and triggered by:
- Changing a slider or dial
- Toggling the servo button
- A timer that periodically sends updates every 200ms
Key Concepts
- Threading: GUI and serial logic must run on different threads to keep the app responsive.
- Signal-Slot Mechanism: PyQt uses signals to safely update UI elements from worker threads.
- QTimer: Used to periodically update servo state without blocking the main loop.
- Encapsulation: Grouping layout, UI creation, and update logic into class methods improves maintainability.
Custom Styling and Appearance
The function apply_dark_burnt_theme()
applies a dark red-themed GUI using QPalette
and CSS-like setStyleSheet()
syntax. This improves visibility and provides a consistent aesthetic.