Interface and Application Programming

Week 14


This week's assignment was to develop an interface that communicates with input and output devices connected to an embedded board previously built.

For this week, I decided to create an interface to monitor the noise level detected by my microphone as an input device. As output devices, I implemented control of a vibration motor module with three different vibration modes and added the ability to turn an LED on and off from the interface as a functionality test. I used a testing board that I previously designed in Week 8, based on a XIAO ESP32-C6.

Group Assignment

Check here the group assignment for this week for more information about interfaces.

Interface

To create the interface, I used Qt Designer. Qt Designer is part of the Qt development ecosystem, a popular cross-platform framework used to develop applications in both C++ and Python through libraries such as PyQt and PySide. It provides a visual environment for designing graphical user interfaces, making it easier to create and organize interface elements without writing the entire layout manually.

The first step is to create a virtual environment on your computer. A virtual environment is an isolated workspace used for software development. It allows a project to have its own libraries, packages, and dependencies without affecting the global Python installation or other projects on the same computer.

Virtual environment

  1. Select or create a folder where the interface code will be stored.
  2. Navigate to the folder and type cmd in the address bar. This will open a terminal already located in the correct directory.
  3. In the terminal, enter the following command to create the virtual environment:
    python -m venv venv
  4. ⚠️
    Note: A new folder containing the virtual environment will be created. It is important not to modify or delete any files inside this folder.
  5. To activate the virtual environment, use the following command:
    venv\Scripts\activate
  6. Once the environment is activated, install the required libraries. This ensures that the dependencies are isolated and do not affect other projects on the computer.
  7. Run the following command to install the necessary libraries:
    pip install pyserial PyQt6

Qt Designer

To begin creating the interface, open Qt Designer.

  1. A window will appear to select a new form. Choose Main Window.
  2. Save your project (.ui) inside the folder you created for the interface development. Make sure to save it outside of the venv folder.
  3. This is the main Qt Designer workspace. It is divided into three main sections:
    • Widget Box: Contains all the interface elements that can be added, such as buttons, text boxes, labels, layouts, etc.
    • Canvas: The area where the graphical interface is designed.
    • Property Editor: Used to modify widget properties such as size, position, colors, fonts, and styles.
  4. To change the background color, right-click on an empty area of the canvas and select Change StyleSheet. Then choose Add Color > Background Color. You can define colors using hexadecimal values or RGB values.
  5. To add buttons, sliders, layouts, text boxes, and other elements, simply drag them from the Widget Box and place them in the desired position on the canvas.
  6. Other style properties that can be customized for buttons, labels, and other widgets include:
    • color: Changes the font color.
    • border-radius: Controls the roundness of corners (expressed in pixels).
    • QPushButton:hover: Changes the button appearance when the mouse hovers over it.
    • QPushButton:pressed: Changes the button appearance when it is clicked.
    Many additional properties can be modified by right-clicking a widget and selecting Change StyleSheet.
  7. This is how my final interface design looks.

Python

  1. Once the interface has been designed and saved in the .ui format, it must be converted into a Python file.
  2. Open the terminal inside the project folder with the virtual environment activated and run the following command:
    pyuic6 -x interface.ui -o frontend.py
  3. This command converts the Qt Designer file (interface.ui) into a Python file (frontend.py) containing all the code required to recreate the graphical interface within a Python application.
  4. Every time changes are made to the interface design in Qt Designer, the .ui file must be converted again into a .py file using the same command.

This generated document handled the programming of the interface's visualization. Therefore, we need to create another document which will contain the interface's logic, since the converted file cannot be edited.

This is the main logic file of the interface. From this point, the programming of the application begins.


                from frontend import *

                class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
                    def __init__(self, *args, **kwargs):
                        QtWidgets.QMainWindow.__init__(self, *args, **kwargs)
                        self.setupUi(self)

                if __name__ == "__main__":
                    app = QtWidgets.QApplication([])
                    window = MainWindow()
                    window.show()
                    app.exec()
            

This is how your project folder should look, containing the virtual environment, the .ui file, and the two .py files.

Upload the Arduino code to your board to handle the logic of the input and output devices. To run the interface, execute the following command in the terminal:

python backend.py

The board will communicate with the interface through the COM port, allowing the application to send commands and receive data in real time.

Python Code


                # Import the serial library used to communicate
                # with the ESP32-C6 through the COM port
                import serial

                # Import PyQt6 modules for the graphical interface
                from PyQt6 import QtWidgets, QtCore

                # Import the interface generated from Qt Designer
                from frontend import *

                # Main application window
                class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):

                    def __init__(self):

                        # Initialize the parent class
                        super().__init__()

                        # Load the graphical interface
                        self.setupUi(self)

                        # Store the currently selected motor mode
                        self.motor_actual = None

                        # Open serial communication with the ESP32-C6
                        self.esp = serial.Serial(
                            'COM3',      # COM port used by the ESP32-C6
                            115200,      # Communication speed
                            timeout=0.1
                        )

                        # LED control buttons
                        self.button_on.clicked.connect(self.led_on)
                        self.button_off.clicked.connect(self.led_off)

                        # Motor control buttons
                        self.button_short.clicked.connect(
                            lambda: self.motor_toggle("SHORT")
                        )

                        self.button_long.clicked.connect(
                            lambda: self.motor_toggle("LONG")
                        )

                        self.button_cont.clicked.connect(
                            lambda: self.motor_toggle("CONT")
                        )

                        # Create a timer that continuously checks
                        # incoming serial data from the ESP32
                        self.timer = QtCore.QTimer()

                        self.timer.timeout.connect(
                            self.leer_serial
                        )

                        # Execute every 50 milliseconds
                        self.timer.start(50)

                    def led_on(self):

                        # Send command to turn the LED on
                        self.esp.write(b"LED_ON\n")

                    def led_off(self):

                        # Send command to turn the LED off
                        self.esp.write(b"LED_OFF\n")

                    def motor_toggle(self, modo):

                        # If the selected mode is already active,
                        # turn the motor off
                        if self.motor_actual == modo:

                            self.esp.write(b"MOTOR_OFF\n")

                            self.motor_actual = None

                        else:

                            # Send the selected motor mode
                            self.esp.write(
                                (modo + "\n").encode()
                            )

                            self.motor_actual = modo

                    def leer_serial(self):

                        # Check if serial data is available
                        while self.esp.in_waiting:

                            # Read one line of data
                            linea = self.esp.readline().decode(
                                errors='ignore'
                            ).strip()

                            # Look for microphone values
                            if linea.startswith("MIC:"):

                                # Extract the numeric value
                                valor = int(
                                    linea.replace("MIC:", "")
                                )

                                # Update the progress bar
                                self.micro.setValue(valor)

                if __name__ == "__main__":

                    # Create the Qt application
                    app = QtWidgets.QApplication([])

                    # Create and show the main window
                    window = MainWindow()

                    window.show()

                    # Start the event loop
                    app.exec()

                

Arduino Code


                // LED connected to pin D2
                #define LED_PIN   D2

                // Vibration motor connected to pin D0
                #define MOTOR_PIN D0

                // Microphone connected to analog pin D1
                #define MIC_PIN   D1

                // Variable used to store incoming serial commands
                String comando = "";

                // Enumeration used to define the available
                // vibration modes for the motor
                enum MotorMode {

                MOTOR_OFF,    // Motor disabled
                MOTOR_SHORT,  // Three short pulses
                MOTOR_LONG,   // Three long pulses
                MOTOR_CONT    // Continuous vibration
                };

                // Store the current motor state
                MotorMode motorMode = MOTOR_OFF;

                void setup() {

                // Initialize serial communication at 115200 baud
                Serial.begin(115200);

                // Configure LED and motor pins as outputs
                pinMode(LED_PIN, OUTPUT);
                pinMode(MOTOR_PIN, OUTPUT);

                // Ensure both devices start turned off
                digitalWrite(LED_PIN, LOW);
                digitalWrite(MOTOR_PIN, LOW);

                // Configure ADC resolution to 12 bits
                analogReadResolution(12);
                }
    
                void loop() {

                // Read commands coming from the Python interface
                while (Serial.available()) {

                    char c = Serial.read();

                    // A complete command is received when a
                    // newline character is detected
                    if (c == '\n') {

                    comando.trim();

                    // Turn LED ON
                    if (comando == "LED_ON") {
                        digitalWrite(LED_PIN, HIGH);
                    }

                    // Turn LED OFF
                    else if (comando == "LED_OFF") {
                        digitalWrite(LED_PIN, LOW);
                    }

                    // Activate short vibration pattern
                    else if (comando == "SHORT") {
                        motorMode = MOTOR_SHORT;
                    }

                    // Activate long vibration pattern
                    else if (comando == "LONG") {
                        motorMode = MOTOR_LONG;
                    }

                    // Activate continuous vibration pattern
                    else if (comando == "CONT") {
                        motorMode = MOTOR_CONT;
                    }

                    // Stop the motor immediately
                    else if (comando == "MOTOR_OFF") {

                        motorMode = MOTOR_OFF;
                        digitalWrite(MOTOR_PIN, LOW);
                    }

                    // Clear command buffer
                    comando = "";
                    }

                    else {

                    // Build the command string character by character
                    comando += c;
                    }
                }
            }
                // Variables used to find the minimum and
                // maximum microphone values
                int minimo = 4095;
                int maximo = 0;

                // Measure microphone activity for 50 ms
                unsigned long inicio = millis();

                while (millis() - inicio < 50) {

                    int lectura = analogRead(MIC_PIN);

                    if (lectura < minimo)
                    minimo = lectura;

                    if (lectura > maximo)
                    maximo = lectura;
                }

                // Calculate signal amplitude
                int amplitud = maximo - minimo;

                // Convert amplitude into a percentage (0-100)
                int level = map(amplitud, 100, 3200, 0, 100);

                // Keep values within valid limits
                level = constrain(level, 0, 100);

                // Apply a smoothing filter to reduce noise
                static int levelFiltrado = 0;

                levelFiltrado =
                    (levelFiltrado * 8 + level * 2) / 10;

                // Send microphone level to the Python interface
                Serial.print("MIC:");
                Serial.println(levelFiltrado);

                switch (motorMode) {

                    // Three short vibration pulses
                    case MOTOR_SHORT:

                    for (int i = 0; i < 3; i++) {

                        digitalWrite(MOTOR_PIN, HIGH);
                        delay(200);

                        digitalWrite(MOTOR_PIN, LOW);
                        delay(200);
                    }

                    motorMode = MOTOR_OFF;
                    break;

                    // Three long vibration pulses
                    case MOTOR_LONG:

                    for (int i = 0; i < 3; i++) {

                        digitalWrite(MOTOR_PIN, HIGH);
                        delay(1000);

                        digitalWrite(MOTOR_PIN, LOW);
                        delay(500);
                    }

                    motorMode = MOTOR_OFF;
                    break;

                    // Continuous vibration for 3 seconds
                    case MOTOR_CONT:

                    digitalWrite(MOTOR_PIN, HIGH);
                    delay(3000);

                    digitalWrite(MOTOR_PIN, LOW);

                    motorMode = MOTOR_OFF;
                    break;

                    // Motor disabled
                    case MOTOR_OFF:

                    digitalWrite(MOTOR_PIN, LOW);
                    break;
                }

                // Small delay before repeating the loop
                delay(50);
                

Final Result