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.

Different interface types

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.
Comparison of interface types

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
Architecture comparison of interface frameworks

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.

GUI made with PyQt

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:

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:

2. Variable and Object Definitions

These are the core objects and variables declared to manage hardware and communication:

3. Setup Function

The setup() function initializes all hardware components:

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.

4.3 Acting on Received Data

Once the incoming values are processed:

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);
        }
Joystick Module layout
Figure 5: Joystick Module pin layout
Wokwi simulation wiring
Figure 6: Wokwi wire and code simulation: Joystick Module
GUI made with PyQt

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.
  • QMainWindow – Main app window
  • QLabel – Text display
  • QSlider – RGB inputs
  • QDial – Angle selector
  • QPushButton – Trigger actions
  • QSpinBox – Numeric input
  • QComboBox – Port selection
  • QVBoxLayout / QHBoxLayout – Organize elements
PyQt5.QtCore Manages signals, threads, and application logic
  • QObject – Base for objects/signals
  • QThread – Custom thread for UART
  • QTimer – Periodic updates
  • pyqtSignal / pyqtSlot – Safe data exchange
PyQt5.QtGui Handles visuals and styles
  • QFont – Font configuration
  • QPalette – Custom GUI colors
  • QIcon – Icon integration
serial / serial.tools.list_ports Detects available COM ports and manages UART communication
  • Serial – Opens and reads/writes to port
  • list_ports.comports() – Detects available ports

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:

The communication works as follows:

  1. When the user clicks Initialize, the COM port is opened with serial.Serial(...).
  2. A separate thread is created to run the reading loop using QThread.
  3. Incoming lines (e.g., "Ax10 Ay0 Az20 Gx5 Gy3 Gz7") are emitted to the main GUI thread.
  4. The GUI parses the data, updates the corresponding labels, and recalculates orientation values (covered later).

Sending data is synchronous and triggered by:

Key Concepts

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.