# Air Quality Monitor **Air Quality Monitor** is a lightweight web interface that displays real-time data from two environmental sensors: **HTU21D** and **BMP180**. The system uses **NGINX** to serve an HTML page that reads live data from JSON files. All the system is runing on a beagle bone black. ## Project Description This project was designed to visualize environmental parameters in a simple, fast, and efficient way using an embedded graphical interface. The data is obtained from the following sensors: - **HTU21D**: Temperature and relative humidity. - **BMP180**: Atmospheric pressure and temperature. The data is stored in two JSON files, updated by the embedded system and displayed through an HTML/JavaScript-based frontend. --- ## Project Structure UI directory - BMP180.json # Pressure and temperature data from BMP180 - HTU21D.json # Temperature and humidity data from HTU21D - index.html # Main web interface - index.js # JavaScript logic to fetch and display JSON data - style.css # Custom CSS styles > The `.json` files are automatically updated by C programs that communicate with the sensors via I2C witch the librarys integrated on the repository. --- ## Conection of the sensors with Beagle bone. Both sensors that we are using are conected to the I2C port 2 of the beagle bone in parallel(P9-19 and P9-20): ![Conection](circuito.jpeg) ## 🚀 Installation & Deployment with NGINX ### Requirements - Linux server with `nginx` installed. - Root access or permission to modify NGINX configuration. ### Steps 1. Install NGINX (if not already installed): ```bash sudo apt update sudo apt install nginx ``` 2. Copy the project files to NGINX’s public directory (e.g., /var/www/html): ``` bash ``` 3. Redirect the configuration file to our repository folder: ```bash sudo vi /etc/nginx/sites-available/default ``` 4. Replace the line: ```bash root /var/www/html; ``` with: ```bash root /home/debian/path/to/your/repository; ``` 5. Apply chages with: ```bash sudo systemctl restart nginx ``` ## User interface This is a simple web page that displays real-time sensor data from two devices: - BMP180: Shows temperature and pressure. - HTU21D: Shows humidity and temperature. It’s styled with a separate CSS file (style.css) and fetches live data using JavaScript (index.js), which likely reads values from two JSON files (BMP180.json and HTU21D.json). ### HTML #### Complete code ```html Sensor Dashboard

Sensor Dashboard

Temperature & Pressure Icon

BMP180

Temperature: -- °C Pressure: -- hPa
Humidity Icon

HTU21D

Humidity: -- % Temperature: -- °C
``` #### Explanation ```html ``` - Declares this document as HTML5. - lang="en" sets the document language to English, which is useful for accessibility and SEO. ##### HEAD ```html Sensor Dashboard ``` - charset="UTF-8" ensures proper text encoding. - viewport makes the layout responsive on mobile devices. - title is what appears in the browser tab. - link imports the external CSS file (style.css) for styling. ##### BODY ```html
...
``` - The background div might be used for a visual effect like a background image or color gradient (defined in your CSS). - container holds the main content (dashboard). - index.js is loaded at the end to ensure the DOM is ready before scripts run. ##### Dashboard Title ```html

Sensor Dashboard

``` - This is the main heading of your web page. ##### Sensor Card: BMP180 ```hmtl
Temperature & Pressure Icon

BMP180

Temperature: -- °C Pressure: -- hPa
``` - This block represents the BMP180 sensor. - The icon (from flaticon.com) visually represents pressure/temperature. - The id attributes (temperature, pressure) are hooks used by JavaScript to insert real values from BMP180.json. ##### Sensor Card: HTU21D ```html
Humidity Icon

HTU21D

Humidity: -- % Temperature: -- °C
``` - This block represents the HTU21D sensor. - The image is a humidity icon. - The id attributes (humidity, tempHTU) are also updated by JavaScript using HTU21D.json. ##### JavaScript Integration ```hmtl ``` - This line loads your script, which is responsible for: - Fetching the JSON files. - Parsing their content. - Replacing the placeholder values (-- °C, -- hPa, etc.) with live sensor data. ### Index.js The script fetches data from two local JSON files: - BMP180.json — contains temperature and pressure. - HTU21D.json — contains temperature and humidity. Then it updates the HTML every 3 seconds so the page always shows live sensor values. #### Full code ```js function updateBMP180() { fetch('BMP180.json') .then(res => res.json()) .then(data => { document.getElementById('temperature').textContent = `Temperature: ${data.temperature.toFixed(2)} °C`; document.getElementById('pressure').textContent = `Pressure: ${data.pressure.toFixed(2)} hPa`; }) .catch(err => console.error("Error BMP180:", err)); } function updateHTU21D() { fetch('HTU21D.json') .then(res => res.json()) .then(data => { document.getElementById('humidity').textContent = `Humidity: ${data.humidity.toFixed(1)} %`; document.getElementById('tempHTU').textContent = `Temperature: ${data.temperature.toFixed(1)} °C`; }) .catch(err => console.error("Error HTU21D:", err)); } setInterval(() => { updateBMP180(); updateHTU21D(); }, 3000); // Carga inicial updateBMP180(); updateHTU21D(); ``` ### Code explanation #### Function: updateBMP180 ```js function updateBMP180() { fetch('BMP180.json') .then(res => res.json()) .then(data => { document.getElementById('temperature').textContent = `Temperature: ${data.temperature.toFixed(2)} °C`; document.getElementById('pressure').textContent = `Pressure: ${data.pressure.toFixed(2)} hPa`; }) .catch(err => console.error("Error BMP180:", err)); } ``` - This defines a new function named updateBMP180. You’ll call this function when you want to update the BMP180 sensor data on the page. - fetch('BMP180.json'): Loads the JSON file from the same directory. - .then(res =\>json()): Parses the response as JSON - .then(data +\> {}): Accesses the data inside the file. - document.getElementById(...) updates the corresponding elements in your HTML. #### Function: updateHTU21D ```js function updateHTU21D() { fetch('HTU21D.json') .then(res => res.json()) .then(data => { document.getElementById('humidity').textContent = `Humidity: ${data.humidity.toFixed(1)} %`; document.getElementById('tempHTU').textContent = `Temperature: ${data.temperature.toFixed(1)} °C`; }) .catch(err => console.error("Error HTU21D:", err)); } ``` Works the same way as updateBMP180(), but: - Fetches from HTU21D.json. - Displays humidity and temperature. - Uses .toFixed(1) for 1 decimal place (common for humidity values). #### Auto-update every 3 seconds ```js setInterval(() => { updateBMP180(); updateHTU21D(); }, 3000); ``` - Calls both update functions every 3,000 milliseconds (3 seconds). - Keeps the UI in sync with new sensor readings, assuming the .json files are being updated continuously. #### Initial load ```js updateBMP180(); updateHTU21D(); ``` - Ensures the data is shown immediately on page load, before the 3-second interval kicks in. ### CSS #### Full code ```css * { margin: 0; padding: 0; box-sizing: border-box; font-family: "Segoe UI", sans-serif; } body, html { height: 100%; background-color: #0d1117; color: #ffffff; position: relative; overflow: hidden; } .background { background-image: url('https://wallpapers.com/images/hd/blue-circuit-board-traces-zn0xezd4t8axj9r6.webp'); background-size: cover; background-position: center; opacity: 0.1; filter: blur(3px); position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; } .container { max-width: 800px; margin: 40px auto; background-color: #161b22; padding: 30px; border-radius: 20px; box-shadow: 0 0 30px rgba(0, 255, 255, 0.2); animation: fadeIn 1s ease-in; } h1 { text-align: center; margin-bottom: 30px; font-size: 2.5rem; color: #58a6ff; } .sensor-card { background-color: #1f2937; padding: 20px; border-radius: 15px; margin-bottom: 20px; box-shadow: 0 0 20px rgba(100, 255, 255, 0.1); animation: slideIn 1s ease; display: flex; flex-direction: column; align-items: center; text-align: center; } .sensor-card img { width: 64px; height: 64px; margin-bottom: 10px; } .sensor-card h2 { margin-bottom: 10px; color: #90cdf4; } .data span { display: block; margin: 5px 0; font-size: 1.2rem; color: #ffffff; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } @keyframes slideIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } ``` #### Explaining Global Styling - *: Resets default browser spacing (margin, padding) and sets a consistent font and box model across the entire page. - body, html: - Sets full height layout. - Applies a dark background (#0d1117) with white text. - Hides overflow and enables positioning for internal layers. ⸻ Background Layer - .background: - Adds a faint, blurred circuit board image as the background. - Uses opacity: 0.1 and filter: blur(3px) to give it a soft tech feel. - Positioned absolutely behind everything (z-index: -1). ⸻ Main Container - .container: - A centered, card-like section with: - A dark background (#161b22) - Rounded corners and soft glowing shadow. - Padding and a fade-in animation on load. ⸻ Title - h1: - Large, centered header in light blue (#58a6ff). - Styled to stand out at the top of the dashboard. ⸻ Sensor Cards - .sensor-card: - Styled boxes for each sensor. - Darker background (#1f2937) with a soft shadow. - Rounded corners, padding, and a slide-in animation when they load. - Contents are centered vertically and horizontally. - .sensor-card img: - Sensor icons sized to 64×64 pixels with margin for spacing. - .sensor-card h2: - Sub-headers for each sensor card in a soft blue (#90cdf4). ⸻ Data Text - .data span: - Each line of sensor data (e.g., temperature, humidity). - Displayed as blocks with spacing and larger font for visibility. ⸻ Animations - @keyframes fadeIn: - Smooth slide-down + fade-in for the container. - @keyframes slideIn: - Subtle zoom-in effect for each sensor card. ## Sensor library ### HTU21D library The library contains 3 files, HTU21D.c, htu21d.h and main.c #### htu21d.h ```h #ifndef HTU21D_H #define HTU21D_H // HTU21D i2c address #define HTU21D_ADDR 0x40 //commands for readings #define HTU21D_TEMP 0xE3 #define HTU21D_HUM 0xE5 #define HTU21D_RESET 0xFE //funtion declarations //Temp: int getTemp(int fd, double *temp); //HUM int getHum(int fd, double *hum); //RESET int getReset(int fd); #endif ``` ##### Explanation - HTU21D_ADDR: The I2C address of the sensor (0x40). - HTU21D_TEMP: Command to read temperature (0xE3). - HTU21D_HUM: Command to read humidity (0xE5). - HTU21D_RESET: Command to reset the sensor (0xFE). - Declares three functions: 1. getTemp() – for reading temperature, 2. getHum() – for reading humidity, 3. getReset() – for resetting the sensor. #### HTU21D.c ```c #include #include #include //Aditional librarys #include #include #include "htu21d.h" #define I2C_PATH "/dev/i2c-%d" #define I2C_PORT 2 int main(){ char filePath[20]; snprintf(filePath, sizeof(filePath), I2C_PATH, I2C_PORT ); int fd = open(filePath, O_RDWR); if(fd<0){ fprintf(stderr, "Error: Unable to access HTU21D sensor: %s",strerror(errno)); exit(-1); } //measurements double temperature=0; double humidity=0; if((getTemp(fd, &temperature)<0)||(getHum(fd, &humidity)<0)){ fprintf(stderr,"Error -404: Measurments not read"); exit(-1); } //printf("HTU21D Module \n"); //printf("%5.2fC \n", temperature); //printf("%5.2fC \n", humidity); printf("{"); printf("\"temperature\": %5.2f,", temperature); printf("\"humidity\": %5.2f", humidity); printf("}"); return 0; } ``` ##### Explanation ###### Librarys ```c #include #include #include #include #include #include "htu21d.h" ``` These headers provide: - unistd.h: basic system calls (like read, write, etc.) - sys/ioctl.h: lets you control devices (e.g., set I2C slave address) - linux/i2c-dev.h: allows interaction with the I2C bus - i2c/smbus.h: provides high-level SMBus/I2C functions like i2c_smbus_read_i2c_block_data - stdio.h: for printing error messages - "htu21d.h": includes your own header file (constants and function declarations) ###### getTemp() Function ```c int getTemp(int fd, double *temp) { getReset(fd); // Resets the sensor before reading char buffer[3]; // Buffer to hold 3 bytes of data from sensor __s32 res = i2c_smbus_read_i2c_block_data(fd, HTU21D_TEMP, 3, buffer); ``` - Calls getReset(fd) to ensure the sensor is in a clean state before reading. - Uses i2c_smbus_read_i2c_block_data to read 3 bytes from the sensor using the temperature command 0xE3. - These 3 bytes contain raw temperature data + CRC (which you ignore here). ```c if (res < 0) { perror("ERROR-1: Failed to read Temperature"); return -1; } ``` - If the read fails (e.g., I2C communication issue), it prints an error and returns -1. ```c *temp = -46.85 + 175.72 * (buffer[0] * 256 + buffer[1]) / 65536.0; return 0; } ``` - The first two bytes in buffer are combined to make a 16-bit raw temperature. - The formula from the HTU21D datasheet is applied to convert raw data into actual temperature in Celsius. - The result is stored in the variable pointed to by *temp. - Returns 0 to indicate success. ###### getHum() Function This function is nearly identical to getTemp(), but it reads humidity instead. ```c int getHum(int fd, double *hum) { getReset(fd); char buffer[3]; __s32 res = i2c_smbus_read_i2c_block_data(fd, HTU21D_HUM, 3, buffer); ``` - Uses the command HTU21D_HUM (value 0xE5) to read humidity data. ```c if (res < 0) { perror("ERROR -3: Failed to read Humidity"); return -1; } *hum = -6 + 125 * (buffer[0] * 256 + buffer[1]) / 65536.0; return 0; } ``` - Applies the formula from the datasheet to convert the raw bytes to % humidity. ###### getReset() Function ```c int getReset(int fd) { if (0 > ioctl(fd, I2C_SLAVE, HTU21D_ADDR)) { perror("ERROR -2: Failed in reset"); return -2; } i2c_smbus_write_byte(fd, HTU21D_RESET); return 0; } ``` - ioctl() tells the I2C driver which slave device you want to talk to (address 0x40). - Then i2c\_smbus\_write\_byte() sends the reset command 0xFE to the HTU21D. - Resets the sensor, which is sometimes necessary to avoid bad reads or stuck states. ### BMP180 library The library contains 3 files, bmp180.c, bmp180.h and main.c #### bmp180.c ```c #include "bmp180.h" #include #include #include #include #include #define BMP180_ADDR 0x77 // Lectura de 16 bits de un registro (dos bytes) static int read16(int fd, uint8_t reg, int16_t *value) { uint8_t buf[2]; if (write(fd, ®, 1) != 1) return -1; if (read(fd, buf, 2) != 2) return -1; *value = (buf[0] << 8) | buf[1]; return 0; } // Escritura de 8 bits a un registro static int write8(int fd, uint8_t reg, uint8_t value) { uint8_t buf[2] = {reg, value}; if (write(fd, buf, 2) != 2) return -1; return 0; } // Leer calibración desde el sensor int bmp180_init(int fd, bmp180_calib_data_t *calib) { if (read16(fd, 0xAA, &calib->AC1) < 0) return -1; if (read16(fd, 0xAC, &calib->AC2) < 0) return -1; if (read16(fd, 0xAE, &calib->AC3) < 0) return -1; if (read16(fd, 0xB0, (int16_t*)&calib->AC4) < 0) return -1; if (read16(fd, 0xB2, (int16_t*)&calib->AC5) < 0) return -1; if (read16(fd, 0xB4, (int16_t*)&calib->AC6) < 0) return -1; if (read16(fd, 0xB6, &calib->B1) < 0) return -1; if (read16(fd, 0xB8, &calib->B2) < 0) return -1; if (read16(fd, 0xBA, &calib->MB) < 0) return -1; if (read16(fd, 0xBC, &calib->MC) < 0) return -1; if (read16(fd, 0xBE, &calib->MD) < 0) return -1; return 0; } // Leer temperatura sin procesar (raw temp) static int bmp180_read_raw_temperature(int fd, int32_t *raw_temp) { if (write8(fd, 0xF4, 0x2E) < 0) return -1; // start temp measurement usleep(4500); // esperar 4.5 ms int16_t value; if (read16(fd, 0xF6, &value) < 0) return -1; *raw_temp = value; return 0; } // Leer presión sin procesar (raw pressure) static int bmp180_read_raw_pressure(int fd, int32_t *raw_press, int oss) { if (write8(fd, 0xF4, 0x34 + (oss << 6)) < 0) return -1; // start pressure measurement usleep(25000); // esperar 25 ms para oss=0 (simple oversampling) uint8_t buf[3]; uint8_t reg = 0xF6; if (write(fd, ®, 1) != 1) return -1; if (read(fd, buf, 3) != 3) return -1; *raw_press = ((buf[0] << 16) | (buf[1] << 8) | buf[2]) >> (8 - oss); return 0; } int bmp180_read_temperature(int fd, bmp180_calib_data_t *calib, double *temperature) { int32_t UT; if (bmp180_read_raw_temperature(fd, &UT) < 0) return -1; int32_t X1 = ((UT - calib->AC6) * calib->AC5) >> 15; int32_t X2 = (calib->MC << 11) / (X1 + calib->MD); int32_t B5 = X1 + X2; *temperature = ((B5 + 8) >> 4) / 10.0; return 0; } int bmp180_read_pressure(int fd, bmp180_calib_data_t *calib, double *pressure) { int oss = 0; // oversampling setting 0..3 int32_t UP; if (bmp180_read_raw_pressure(fd, &UP, oss) < 0) return -1; // Recalcular B5 para temperatura, necesario para presión int32_t UT; if (bmp180_read_raw_temperature(fd, &UT) < 0) return -1; int32_t X1 = ((UT - calib->AC6) * calib->AC5) >> 15; int32_t X2 = (calib->MC << 11) / (X1 + calib->MD); int32_t B5 = X1 + X2; int32_t B6 = B5 - 4000; X1 = (calib->B2 * ((B6 * B6) >> 12)) >> 11; X2 = (calib->AC2 * B6) >> 11; int32_t X3 = X1 + X2; int32_t B3 = (((calib->AC1 * 4 + X3) << oss) + 2) >> 2; X1 = (calib->AC3 * B6) >> 13; X2 = (calib->B1 * ((B6 * B6) >> 12)) >> 16; X3 = ((X1 + X2) + 2) >> 2; uint32_t B4 = (calib->AC4 * (uint32_t)(X3 + 32768)) >> 15; uint32_t B7 = ((uint32_t)UP - B3) * (50000 >> oss); int32_t p; if (B7 < 0x80000000) { p = (B7 << 1) / B4; } else { p = (B7 / B4) << 1; } X1 = (p >> 8) * (p >> 8); X1 = (X1 * 3038) >> 16; X2 = (-7357 * p) >> 16; p = p + ((X1 + X2 + 3791) >> 4); *pressure = p / 100.0; // Pa a hPa (mbar) return 0; } ``` ##### explanation ###### Headers and defines ```c #include "bmp180.h" #include #include #include #include #include #define BMP180_ADDR 0x77 ``` - Includes standard and I2C Linux headers. - Defines the I2C address of the BMP180 sensor. ###### read16() – Read 16-bit (2-byte) value from a register ```c static int read16(int fd, uint8_t reg, int16_t *value) ``` - Sends the register address via write(). - Reads 2 bytes and stores the result as a big-endian 16-bit signed integer. - Used to read calibration values and sensor data. ###### write8() – Write 8-bit value to a register ```c static int write8(int fd, uint8_t reg, uint8_t value) ``` - Sends 2 bytes: register address and value. - Used to trigger temperature or pressure measurements by writing command codes to control register 0xF4 ###### bmp180\_init() – Read all calibration data from sensor ```c int bmp180_init(int fd, bmp180_calib_data_t *calib) ``` - Reads 11 calibration values from registers 0xAA to 0xBF. - These are factory-set and unique to each sensor. - Needed to apply temperature and pressure compensation formulas correctly. ###### bmp180\_read\_raw\_temperature() – Trigger and read unprocessed temperature ```c static int bmp180_read_raw_temperature(int fd, int32_t *raw_temp) ``` - Writes 0x2E to control register 0xF4 to start temperature conversion. - Waits 4.5 ms (conversion time). - Reads result from 0xF6 and 0xF7. ###### bmp180\_read\_raw\_pressure() – Trigger and read unprocessed pressure ```c static int bmp180_read_raw_pressure(int fd, int32_t *raw_press, int oss) ``` - Writes 0x34 + (oss << 6) to register 0xF4 to start pressure conversion. - Waits 25 ms for conversion (OSS=0). - Reads 3 bytes from 0xF6, 0xF7, 0xF8 and shifts them to get a 19-bit value. - oss (oversampling setting) affects resolution and delay. ###### bmp180\_read\_temperature() – Convert raw temperature to °C ```c int bmp180_read_temperature(int fd, bmp180_calib_data_t *calib, double *temperature) ``` - Uses raw temperature and calibration data. - Follows Bosch’s datasheet compensation formula: X1 = ((UT - AC6) * AC5) >> 15 X2 = (MC << 11) / (X1 + MD) B5 = X1 + X2 T = (B5 + 8) >> 4 → gives temperature in 0.1°C - Final result is divided by 10.0 to get °C. ###### bmp180\_read\_pressure() – Convert raw pressure to hPa ```c int bmp180_read_pressure(int fd, bmp180_calib_data_t *calib, double *pressure) ``` - Uses raw pressure and temperature (needed for B5). - Applies long compensation formula from datasheet: 1. Many intermediate steps using calibration values. 2. Computes B3, B4, B6, B7, X1, X2, X3, and finally p. - Final pressure p is in Pa; divide by 100.0 to get hPa (mbar). #### bmp180.h ```c #ifndef BMP180_H #define BMP180_H #include typedef struct { int16_t AC1, AC2, AC3; uint16_t AC4, AC5, AC6; int16_t B1, B2; int16_t MB, MC, MD; } bmp180_calib_data_t; int bmp180_init(int fd, bmp180_calib_data_t *calib); int bmp180_read_temperature(int fd, bmp180_calib_data_t *calib, double *temperature); int bmp180_read_pressure(int fd, bmp180_calib_data_t *calib, double *pressure); #endif ``` ##### Explanation ###### Header ```h #ifndef BMP180_H #define BMP180_H #include ``` - Header guard to prevent multiple inclusion. - Includes standard integer types (uint8_t, int16_t, etc.). ###### Structure: bmp180\_calib\_data\_t ```h typedef struct { int16_t AC1, AC2, AC3; uint16_t AC4, AC5, AC6; int16_t B1, B2; int16_t MB, MC, MD; } bmp180_calib_data_t; ``` - Holds all calibration constants (11 total). - Types match datasheet: some are signed, others unsigned. ###### Function declarations ```h int bmp180_init(int fd, bmp180_calib_data_t *calib); int bmp180_read_temperature(int fd, bmp180_calib_data_t *calib, double *temperature); int bmp180_read_pressure(int fd, bmp180_calib_data_t *calib, double *pressure); ``` #### main.c ```c #include #include #include #include #include #include "bmp180.h" #define I2C_BUS "/dev/i2c-2" #define BMP180_ADDR 0x77 int main() { int fd = open(I2C_BUS, O_RDWR); if (fd < 0) { perror("Error abriendo el bus I2C"); return 1; } if (ioctl(fd, I2C_SLAVE, BMP180_ADDR) < 0) { perror("Error configurando la dirección I2C"); close(fd); return 1; } bmp180_calib_data_t calib; if (bmp180_init(fd, &calib) < 0) { fprintf(stderr, "Error leyendo datos de calibración BMP180\n"); close(fd); return 1; } double temperature, pressure; if (bmp180_read_temperature(fd, &calib, &temperature) < 0) { fprintf(stderr, "Error leyendo temperatura BMP180\n"); close(fd); return 1; } if (bmp180_read_pressure(fd, &calib, &pressure) < 0) { fprintf(stderr, "Error leyendo presión BMP180\n"); close(fd); return 1; } printf("{\"temperature\": %.2f, \"pressure\": %.2f}\n", temperature, pressure); close(fd); return 0; } ``` ##### Explanation ###### Open I2C Bus ```c int fd = open("/dev/i2c-2", O_RDWR); ``` - Opens I2C device for read/write. ###### Set I2C Slave Address ```c ioctl(fd, I2C_SLAVE, BMP180_ADDR); ``` - Tells the kernel which I2C device address (0x77) to communicate with. ###### Initialize BMP180 ```c bmp180_init(fd, &calib); ``` - Reads calibration constants from the sensor. ###### Read Temperature and Pressure ```c bmp180_read_temperature(fd, &calib, &temperature); bmp180_read_pressure(fd, &calib, &pressure); ``` - Uses previously stored calibration values to read and convert real temperature and pressure values. ###### Output in JSON Format ```c printf("{\"temperature\": %.2f, \"pressure\": %.2f}\n", temperature, pressure); ``` - Very useful becouse we are integrating with a frontend (e.g. web UI or logger). ###### Cleanup ```c close(fd); ``` - Closes the I2C device after finishing communication. ## Results Photo to prove the functionality of the UI: ![Implemented UI](sistema.jpeg) Screenshot of UI: ![Screenshot](Prueba.png)