This week I programmed the interface for my final proyect, in which you can see the indoor and outdoor air quality data to compare them and know what to do.
Design, build, and connect a wired and/or wireless interface for a final project application. Document the complete development process.
This week focused on developing visual interfaces. In class we explored how to build desktop GUIs using the Python Qt library ecosystem. Here are my class notes on how to do a Virtual enviroment and a interface with Qt designer.
There are many available libraries; we focused on Python Qt. Python alone cannot build visual interfaces, but with the right libraries it can. pyQt6 is the package used to create those interfaces.
A virtual environment is a local directory where you can install libraries for a specific project, you create a bubble where you install all the versions you need. If you do not use it, the bubble only occupies memory. The advantage is that all the libraries the project needs are contained inside that bubble. Installing a library at the system level would affect all projects, but here it does not because the project's library is contained inside the bubble. You can have multiple bubbles.
Windows + R, type cmd, press Enter.
python3. If you see something like Python 3.14.4 … on darwin, it is installed.
cd (e.g., desktop). cd is used to enter folders.
mkdir. Example: mkdir interfaces_fab. ls shows the files inside a folder; pwd shows your current location.
python -m venv venv / Mac: python3 -m venv venv. The second "venv" is the name given to the environment. If it succeeds, nothing is returned.
source venv/bin/activate / Windows: venv\scripts\activate. This step is done only once after the folder is created; you do not need to repeat the previous steps.
pip install PyQt6
pip freeze — shows the package name and what was installed directly in the virtual environment.
pip freeze > requirements.txt so that anyone who installs your project gets the same libraries you had on your machine. cat requirements.txt shows the file contents in the console.
pip install pyserial
code .
Cmd + R to preview the interface.
pyuic6 -x interfaz.ui -o frontend.py. This changes the language from the UI file to Python and renames it. Super tip: never use spaces in file names.
python frontend.py. If everything worked correctly, your interface will open from the terminal.
Frontend file: stores the interface design — everything the user sees and interacts with directly. This file comes directly from Qt Designer. Do not modify it.
Backend file: uses the frontend file as a library. Stores the logic and data that make the application run. It runs on a web server and is invisible to the end user. This is where changes are made to adapt everything to the sensor.
Create a new file in Visual Studio Code named backend.py and paste the following code:
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()
If you want it to connect with your microcontroller you can use the backend communicates with the microcontroller through serial communication (UART). The pyserial library allows you to read the prints from the microcontroller.
This is a video of a parto of my Qt designing process:
This is what we covered in class. I found the part about creating a virtual environment and using Qt Designer to generate the UI file directly very interesting. However, when developing the idea for my project, I realized that even though the virtual environment approach could be useful, what they taught us ultimately creates a desktop application and I was looking for something more versatile that could be accessed from a web browser. Because of this I started looking for alternatives and found the solution: build the page in HTML format, as I had done during the communications week, and host it through a server.
From the interface design stage, I wanted to create something meaningful: I wanted the entire project to tell a story connected directly to the physical object. My process started with a brainstorm. At first, my intention was for the web page's aesthetic to be inspired by the origami flower. However, as I iterated on the idea, I realized that integrating a bee as the digital protagonist would add a beautiful and complementary dynamism to the project.
I chose a bee because it makes the perfect match with the physical origami flower I built. In nature, bees and flowers have an inseparable connection and furthermore, bees are extremely sensitive to air pollution. I found it an amazing idea that the bee acts as our guardian on screen: if the air is clean, she is happy and working; but if pollution levels rise, the bee alerts us digitally at exactly the moment the origami flower reacts and moves in the real world. It was the best way to connect what happens on the web with the physical mechanism, making the entire project feel alive.
I searched for dashboard design ideas and started experimenting in Canva to create an aesthetic dashboard aligned with the narrative. After testing different configurations, I settled on this design:
To create the bee graphic, I started by making several sketches. I explored different forms and the option I liked most was a bee built from geometric shapes, since it follows the same visual rhythm as the origami structure.
After choosing the one I liked best, I moved to Procreate to draw it and get a sense of how it would look on the web page. I made 4 different variants, each indicating a different air quality scenario: open the window because interior air quality is bad, everything is fine, or the window should stay closed and the air purifier should be turned on. I started them in Procreate.
These Procreate drawings were then passed directly to Vizcom due to time constraints, to generate the images in the desired style. Afterwards, I traced over them in Affinity to achieve better quality.
I traced the logo myself in Affinity. Here is a video of the process:
To be able to get the best help from claude, i first defined everything I wanted for the proyect. The interface acts as a control and visualization center. Below is a breakdown of each major component of the system.
Uses MQTT over WebSockets to receive data from the XIAO and chart air quality throughout the day (Daily chart — bottom panel). This is reflected in 3 different values: CO2, VOC, and Temperature, found in the Local Sensor panel. The card color changes based on whether the value is good (green), medium (yellow), or bad (red).
The web page consumes a government API to display the exterior air quality of the city, allowing a comparison of both environments on the same screen. In the Exterior Air Quality panel, you can see how long ago it was last updated, the general air quality in a white box reading Good, Medium or Bad. Below there are 3 progress charts representing the air quality data vertically — the first shows the general air quality index, the second the PM 2.5 amount, and the third PM 10. As values rise, the progress chart changes color between green, yellow, and red.
The system evaluates danger based on two metrics obtained from the sensors, and based on this the panel in the right corner shows a phrase with an animation:
Once I had the design, I searched for the API from which to pull exterior air quality data. These were my three finalist options: Ambee (getambee.com), OpenWeather (openweathermap.org), and OpenAQ (openaq.org).
I initially chose Ambee because it works through AI-based estimation, giving precise data for any given location. However, upon further research I found that it only provides a 15-day trial before requiring payment. Because of this I chose OpenWeather — its data is extremely stable and has global coverage, and its free tier is permanent and generous (up to 60 requests per minute).
http://api.openweathermap.org/data/2.5/air_pollution?lat=[YOUR_LATITUDE]&lon=[YOUR_LONGITUDE]&appid=[YOUR_API_KEY]
To get your coordinates, open Google Maps, right-click on the blue dot on your location, and copy the data.
After having the design and the API, I moved on to building the interface prompt with the help of AI. I went through several iterations, from a complex, broken version to a clean, working dashboard.
At first I wanted the data to be stored in a database for the user login. I found Supabase, created my account, and generated my project URL. However, I spent a lot of time fighting with the AI to build the interface, and in the end it turned out too complex and did not work properly.
To solve the login issue, I opted for a simpler approach. It works like this: when you fill in the SIGN UP form and click the button, the code takes everything you typed and saves it in your own browser with this line:
localStorage.setItem('aqm_user', JSON.stringify({ name, mail, pass, coords }));
localStorage is like a small box that your browser has to save information even after you close the tab. Your name, email, password, and coordinates are stored there. When you open the page again, the code checks if that box has anything saved.
I had a naming conflict in the HTML that prevented the page from loading. I went back to the prompt and simplified it — I wanted to start from the basics to better understand what was happening and gradually fix the errors. This approach worked much better: it avoided having a very long piece of code and helped me understand overall what was being done. The first interface the AI gave me had all these errors, found by right-clicking on the page and selecting Inspect:
Failed to load resource: the server responded with a status of 404 (Not Found)
Outdoor fetch failed: TypeError: Cannot set properties of null (setting 'src')
at updateRecommendation (TryInterfaces.html:639:53)
at applyOutdoor (TryInterfaces.html:686:3)
at fetchOutdoor (TryInterfaces.html:669:5)
Uncaught (in promise) TypeError: Cannot set properties of null (setting 'src')
at updateRecommendation (TryInterfaces.html:639:53)
at t.<anonymous> (mqtt.min.js:2:10257)
Uncaught Error: Canvas is already in use.
Chart with ID '0' must be destroyed before the canvas with ID 'gaugeCO2' can be reused.
After resolving those errors, I managed to display the XIAO data on the page and have the panel change color based on the reading. However, several outstanding problems remained:
After simulating, I managed to connect to the XIAO, but the exterior air quality data looked wrong. I checked the OpenWeather values table and realized that the values on my page were appearing as decimals, while in reality they should appear as whole numbers the result was the number I was getting multiplied by two decimal places.
After several iterations, I achieved the final working interface. Here is a video of how it works:
The libraries are imported at the beginning of the file in these three lines:
<link href="https://fonts.googleapis.com/css2?family=Poppins...">
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
The API key is defined at the beginning of the script:
const API_KEY = 'Your API key';
And it is used in the line where the request is assembled:
const url = `https://api.openweathermap.org/data/2.5/air_pollution?lat=${lat}&lon=${lon}&appid=${API_KEY}`;
It sends your latitude and longitude to OpenWeather along with the key that identifies you as a service user, and OpenWeather responds with the air quality data for that location.
The final code is contained entirely in one HTML file, with JavaScript and CSS integrated within it.
When you open the file in the browser, the first thing that loads are three external resources: the Poppins font, the library that allows connecting to the sensor over the internet, and the library that draws the charts. Without these three things, the dashboard would not work.
The first thing you see is the login screen with two tabs: LOGIN and SIGN UP. If it is your first time, fill in the SIGN UP with your name, email, password, and the coordinates of where you are. Those coordinates are important because they are later used to query the exterior air quality of your exact location. All of that is saved in the browser, so the next time you open the page it does not ask you to fill it in again — it takes you directly to the dashboard.
As soon as you enter the dashboard, several things happen at the same time: the three semicircular gauge charts for the sensors are created, the bottom line chart is created, it connects to the XIAO sensor over the internet, and it requests exterior air quality data from OpenWeather using the coordinates you saved.
The XIAO sensor publishes data continuously over the internet using a protocol called MQTT — basically a messaging system. The dashboard connects to that same system and subscribes to three channels: one for CO2, one for VOC, and one for temperature. Every time the sensor sends a new number, the dashboard receives it automatically and updates everything on screen.
To know whether the sensor is on or off, the code uses a 45-second timer. Every time a sensor reading arrives, that timer resets. If 45 seconds go by without receiving anything, the badge changes to "No Signal" in yellow. If the sensor sends data, it stays on "Connected" in green. If the internet connection drops completely, it changes to "Disconnected" in red.
When a new CO2 or VOC number arrives, the code first calculates the cumulative average of all values that have arrived since the dashboard was opened. Then it compares against defined ranges:
Based on the result, the card changes color to green, yellow, or red and the badge reads GOOD, MEDIUM, or BAD.
The code combines the CO2 and VOC status to produce a single number representing indoor air quality. If either of them is red, the entire interior quality is bad. If neither is red but one is yellow, it is medium. It is only good if both are green at the same time.
Every time a sensor reading arrives, the code records whether the interior quality at that moment was good (1), medium (2), or bad (3). It groups those records into 5-minute intervals and calculates the average for each. This way the chart grows on its own while the sensor is sending data, and you can see how air quality changed throughout the day.
When entering the dashboard, the code takes the coordinates saved during login and asks the OpenWeather API for the air quality at that point. OpenWeather responds with a number from 1 to 5 and the exact values of PM2.5 and PM10 in µg/m³. The code translates that number into text (GOOD, FAIR, MODERATE, POOR, or VERY POOR) and colors the three cards in the right panel according to the official ranges of the European Union air quality table. This query repeats automatically every 10 minutes.
This is the part that brings everything together. The code takes the interior quality calculated from the sensor and the exterior quality obtained from the API, and decides which image to show and what text to display:
This recommendation updates every time a new sensor reading arrives or every time the exterior weather information is refreshed.
This week involved a lot of trial and error. Even though AI is a great tool, most of the time it will give you a result but not exactly the one you want. No matter how much information you give it, if you do not understand what it is doing and something goes wrong, fixing it will cost you much more time. On top of that, if you let it do all the work you do not learn as much. I am in favor of using AI, as long as you review what it gives you, try to understand it, and then make the modifications you need. That is the process that I tried following during this week, i found it more complicated than the networking and communications week.