Project

General

Profile

interfaz.py

Esta es la interfaz grafica del funcionamiento de los sensores - lukas torres, 01/02/2026 03:01 PM

Download (21.4 KB)

 
1
import grovepi
2
import time
3
import math
4
from datetime import datetime
5
import tkinter as tk
6
from tkinter import ttk, messagebox
7
import threading
8
import os
9
import pandas as pd
10
import matplotlib.pyplot as plt
11
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
12

    
13
# --- TUS VALORES R0 CALCULADOS ---
14
R0_MQ7 = 5451.23
15
R0_MQ4 = 21159.60
16
R0_MQ135 = 14967.54
17
# -----------------------------------
18

    
19
# --- CONFIGURACIÓN DE PINES Y CONSTANTES ---
20
MQ7_PIN = 0     
21
MQ4_PIN = 1     
22
MQ135_PIN = 2   
23
BUZZER_PIN = 7  
24
NOMBRE_ARCHIVO = 'database.txt' 
25
RL_VALUE = 10000.0
26
MAX_ANALOG = 1023.0
27
TIEMPO_LECTURA_MS = 1000 
28

    
29
# LÍMITES DE ESCALA PARA MEDIDORES PPM
30
MAX_CO_DISPLAY = 100        
31
MAX_PPM_DISPLAY = 2000      
32

    
33
# Variables de control de estado
34
MEDICION_ACTIVA = False 
35
ALARMA_ACTIVA = False
36

    
37
# --- VARIABLES GLOBALES DE UMBRAL ---
38
UMBRAL_CO_DEFECTO = 50.0 
39
UMBRAL_CH4_DEFECTO = 1000.0 
40
UMBRAL_AIRE_DEFECTO = 1000.0 
41

    
42
UMBRAL_CO = UMBRAL_CO_DEFECTO
43
UMBRAL_CH4 = UMBRAL_CH4_DEFECTO
44
UMBRAL_AIRE = UMBRAL_AIRE_DEFECTO
45

    
46
# --- DECLARACIÓN DE VARIABLES GLOBALES DE WIDGETS ---
47
# Declaradas para evitar NameError en las funciones
48
canvas_mq7 = None
49
canvas_mq4 = None
50
canvas_mq135 = None
51
estado_general_label = None
52
btn_iniciar_mediciones = None
53
btn_iniciar_inicio = None
54
notebook = None
55

    
56
# ----------------------------------------------------
57
# FUNCIONES MATEMÁTICAS Y DE SENSORES
58
# ----------------------------------------------------
59

    
60
def get_RS_resistance(analog_value):
61
    if analog_value == 0: return float('inf')
62
    return RL_VALUE * ((MAX_ANALOG / analog_value) - 1.0)
63

    
64
def MQ7_to_PPM(rs, R0):
65
    if R0 == 0: return 0.0
66
    ratio = rs / R0
67
    if ratio >= 0.783:
68
        return 10**((math.log10(ratio) - 0.783) / -1.531)
69
    return 0.0
70

    
71
def MQ4_to_PPM(rs, R0):
72
    if R0 == 0: return 0.0
73
    ratio = rs / R0
74
    if ratio <= 2.5:
75
        return 10**((math.log10(ratio) - 2.5) / -1.5)
76
    return 0.0
77

    
78
def MQ135_to_PPM(rs, R0):
79
    if R0 == 0: return 0.0
80
    ratio = rs / R0
81
    if ratio < 0.0000001:
82
        return 0.0
83
    if ratio <= 2.5:
84
        return 10**((math.log10(ratio) - 2.5) / -2.5)
85
    return 0.0
86

    
87
def calcular_aqi_co(ppm_co):
88
    """Calcula un AQI simplificado (Mantenido solo por consistencia de la función)."""
89
    ppm = ppm_co
90
    
91
    CO_GOOD_PPM = 4.4
92
    CO_FAIR_PPM = 9.4
93
    CO_POOR_PPM = 12.4
94

    
95
    if ppm <= CO_GOOD_PPM:
96
        aqi = int(round(ppm * (50 / CO_GOOD_PPM)))
97
    elif ppm <= CO_FAIR_PPM:
98
        aqi = 51 + int(round(((ppm - CO_GOOD_PPM) / (CO_FAIR_PPM - CO_GOOD_PPM)) * (100 - 51)))
99
    elif ppm <= CO_POOR_PPM:
100
        aqi = 101 + int(round(((ppm - CO_FAIR_PPM) / (CO_POOR_PPM - CO_FAIR_PPM)) * (150 - 101)))
101
    else:
102
        aqi = 151 + int(ppm * 10) 
103
    
104
    return aqi, "N/A", "gray" 
105

    
106

    
107
# ----------------------------------------------------
108
# FUNCIONES DE CONTROL DE LA GUI
109
# ----------------------------------------------------
110

    
111
def guardar_datos(ppm_7, ppm_4, ppm_135, estado_alarma):
112
    """Escribe los datos en el archivo database.txt."""
113
    try:
114
        timestamp_completo = datetime.now().strftime('%d-%m-%Y | %H:%M:%S')
115
        
116
        datos_a_escribir = f"""
117
FECHA: {timestamp_completo}
118
ALARMA: {estado_alarma}
119
---
120
MQ-7 (CO)   : {ppm_7:.2f} PPM
121
MQ-4 (CH4)  : {ppm_4:.2f} PPM
122
MQ-135 (Aire): {ppm_135:.2f} PPM
123
---------------------------------"""
124
        
125
        with open(NOMBRE_ARCHIVO, 'a', encoding="utf-8") as archivo:
126
            archivo.write(datos_a_escribir)
127
            archivo.write("\n")
128

    
129
    except Exception as e:
130
        print(f"Error de escritura en archivo: {e}")
131

    
132
def aplicar_umbrales():
133
    """Lee y valida los valores de los Entry y los actualiza globalmente."""
134
    global UMBRAL_CO, UMBRAL_CH4, UMBRAL_AIRE
135
    try:
136
        UMBRAL_CO = float(entry_co_var.get())
137
        UMBRAL_CH4 = float(entry_ch4_var.get())
138
        UMBRAL_AIRE = float(entry_aire_var.get())
139
        
140
        if UMBRAL_CO <= 0 or UMBRAL_CH4 <= 0 or UMBRAL_AIRE <= 0:
141
            raise ValueError("Los umbrales deben ser mayores que cero.")
142

    
143
        tk.messagebox.showinfo("Configuración", "Umbrales guardados con éxito.")
144

    
145
    except ValueError as e:
146
        tk.messagebox.showerror("Error de Configuración", f"Valor inválido: {e}. Use números.")
147

    
148
def reset_umbrales():
149
    """Restaura los umbrales a los valores de seguridad por defecto."""
150
    entry_co_var.set(UMBRAL_CO_DEFECTO)
151
    entry_ch4_var.set(UMBRAL_CH4_DEFECTO)
152
    entry_aire_var.set(UMBRAL_AIRE_DEFECTO)
153
    aplicar_umbrales() 
154

    
155
def limpiar_historial():
156
    """Borra el archivo database.txt para iniciar un nuevo historial."""
157
    if os.path.exists(NOMBRE_ARCHIVO):
158
        os.remove(NOMBRE_ARCHIVO)
159
        tk.messagebox.showinfo("Historial", f"Historial '{NOMBRE_ARCHIVO}' limpiado con éxito.")
160
    else:
161
        tk.messagebox.showinfo("Historial", "El archivo de historial ya estaba vacío.")
162

    
163
# --- FUNCIONES DE DIBUJO Y NAVEGACIÓN ---
164

    
165
def dibujar_medidor_ppm(canvas, ppm_valor, umbral, max_escala, titulo):
166
    """Dibuja el medidor radial para un valor PPM dado. (Lógica de umbral corregida)"""
167
    if canvas is None: return 
168
    ancho = 200
169
    alto = 120
170
    centro_x = ancho / 2
171
    centro_y = alto 
172
    radio = 80
173
    
174
    canvas.delete("all")
175
    canvas.config(bg="#F0F0F0") 
176
    
177
    escala_angular = 180 / max_escala
178
    
179
    # 1. Dibujar el arco de fondo
180
    canvas.create_arc(centro_x - radio, centro_y - radio, 
181
                      centro_x + radio, centro_y + radio,
182
                      start=0, extent=180, style=tk.ARC, outline="#DDDDDD", width=15)
183
    
184
    # 2. Ángulo del Valor Medido (Arco verde/rojo)
185
    angulo_valor_ppm = min(ppm_valor, max_escala) * escala_angular
186
    angulo_inicio_valor = 180 - angulo_valor_ppm # 180 = 0 PPM
187
    
188
    color = "red" if ppm_valor >= umbral else "green"
189
    
190
    # Dibujar el arco de valor medido
191
    canvas.create_arc(centro_x - radio, centro_y - radio, 
192
                      centro_x + radio, centro_y + radio,
193
                      start=angulo_inicio_valor, extent=angulo_valor_ppm,
194
                      style=tk.ARC, outline=color, width=15)
195

    
196
    # 3. Ángulo del Umbral (Línea roja punteada)
197
    angulo_umbral_total = umbral * escala_angular
198
    
199
    # CORRECCIÓN DEFINITIVA: 
200
    # Queremos que 0 PPM esté en 180 grados. 
201
    # Queremos que Max PPM esté en 0 grados.
202
    # El ángulo que necesitamos para cos/sin es 180 - (Valor * Escala)
203
    angulo_umbral_pos = 180 - angulo_umbral_total
204
    
205
    # Convertir a radianes
206
    rad_umbral = math.radians(angulo_umbral_pos)
207
    
208
    # Coordenadas de la punta de la línea de umbral
209
    punta_x_u = centro_x - (radio - 25) * math.cos(rad_umbral)
210
    punta_y_u = centro_y - (radio - 25) * math.sin(rad_umbral)
211
    
212
    # Dibujar la línea de umbral
213
    canvas.create_line(centro_x, centro_y, punta_x_u, punta_y_u, 
214
                       fill="red", width=2, dash=(3, 3)) 
215
    
216
    # 4. Dibujar el valor central y etiquetas (Más grande y visible)
217
    canvas.create_text(centro_x, centro_y - 45, text=titulo, 
218
                       font=("Helvetica", 12, "bold"), fill="#333333")
219
    canvas.create_text(centro_x, centro_y - 20, text=f"{ppm_valor:.1f}", 
220
                       font=("Helvetica", 24, "bold"), fill=color) 
221
    canvas.create_text(centro_x, centro_y + 5, text="PPM", 
222
                       font=("Helvetica", 12), fill="#333333") 
223
    
224
    canvas.create_text(centro_x - radio - 10, centro_y - 5, text="0", fill="#333333")
225
    canvas.create_text(centro_x + radio + 5, centro_y - 5, text=str(max_escala), fill="#333333")
226

    
227

    
228
def abrir_historial():
229
    notebook.select(historial_frame)
230
    generar_grafico() 
231

    
232
def abrir_configuracion():
233
    notebook.select(opciones_frame)
234

    
235
def abrir_mediciones():
236
    notebook.select(mediciones_frame)
237
    
238
def salir_limpiar():
239
    grovepi.digitalWrite(BUZZER_PIN, 0)
240
    root.destroy()
241
    
242
# --- Funciones de Medición y Control ---
243

    
244
def iniciar_medicion():
245
    """Inicia el ciclo de lectura de sensores."""
246
    global MEDICION_ACTIVA
247
    if not MEDICION_ACTIVA:
248
        aplicar_umbrales() 
249
        MEDICION_ACTIVA = True
250
        # Actualizar ambos botones de inicio/detención
251
        btn_iniciar_mediciones.config(text="Detener Medición", command=detener_medicion, style='Red.TButton')
252
        btn_iniciar_inicio.config(text="Detener Medición", command=detener_medicion, style='Red.TButton')
253
        estado_general_label.config(text="Monitor Activo", background="blue")
254
        root.after(0, actualizar_sensores) # Inicia el bucle de Tkinter
255

    
256
def detener_medicion():
257
    """Detiene el ciclo de lectura de sensores."""
258
    global MEDICION_ACTIVA
259
    MEDICION_ACTIVA = False
260
    # Actualizar ambos botones de inicio/detención
261
    btn_iniciar_mediciones.config(text="Empezar Medición", command=iniciar_medicion, style='Green.TButton')
262
    btn_iniciar_inicio.config(text="Empezar Medición", command=iniciar_medicion, style='Green.TButton')
263
    estado_general_label.config(text="Monitor Detenido", background="gray")
264

    
265

    
266
# ----------------------------------------------------
267
# GENERACIÓN DE GRÁFICOS (Se mantiene igual)
268
# ----------------------------------------------------
269

    
270
def generar_grafico():
271
    """Lee el archivo de texto y genera TRES gráficos con Matplotlib, con escala optimizada."""
272
    
273
    def leer_datos_y_estructurar_todos():
274
        datos = []
275
        try:
276
            with open(NOMBRE_ARCHIVO, 'r', encoding='utf-8') as f:
277
                contenido = f.read()
278
            
279
            patrones = contenido.split("---------------------------------")
280
            for patron in patrones:
281
                if 'MQ-7 (CO)' in patron:
282
                    try:
283
                        fecha_str = patron.split("FECHA: ")[1].split('\n')[0].strip()
284
                        
285
                        ppm_7 = float(patron.split('MQ-7 (CO)   : ')[1].split(' PPM')[0].strip())
286
                        ppm_4 = float(patron.split('MQ-4 (CH4)  : ')[1].split(' PPM')[0].strip())
287
                        ppm_135 = float(patron.split('MQ-135 (Aire): ')[1].split(' PPM')[0].strip())
288
                        
289
                        fecha = datetime.strptime(fecha_str, '%d-%m-%Y | %H:%M:%S')
290
                        
291
                        datos.append({
292
                            'Fecha': fecha, 'PPM_CO': ppm_7, 'PPM_CH4': ppm_4, 'PPM_AIRE': ppm_135
293
                        })
294
                    except Exception:
295
                        continue 
296
        except FileNotFoundError:
297
            return pd.DataFrame({'Fecha': [], 'PPM_CO': [], 'PPM_CH4': [], 'PPM_AIRE': []})
298
        
299
        return pd.DataFrame(datos)
300

    
301
    df = leer_datos_y_estructurar_todos()
302
    
303
    if df.empty:
304
        tk.messagebox.showwarning("Gráfico", "No hay datos suficientes para graficar.")
305
        return
306

    
307
    # 2. Configurar Matplotlib para Tkinter (Figura con 3 subplots)
308
    fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 8)) 
309
    plt.subplots_adjust(hspace=0.4) 
310

    
311
    # 3. Datos y Umbrales
312
    sensores_info = [
313
        {'col': 'PPM_CO', 'titulo': 'MQ-7 (Monóxido de Carbono)', 'umbral': UMBRAL_CO},
314
        {'col': 'PPM_CH4', 'titulo': 'MQ-4 (Metano)', 'umbral': UMBRAL_CH4},
315
        {'col': 'PPM_AIRE', 'titulo': 'MQ-135 (Calidad del Aire)', 'umbral': UMBRAL_AIRE}
316
    ]
317

    
318
    # 4. Generar cada gráfico
319
    for i, info in enumerate(sensores_info):
320
        ax = axes[i]
321
        
322
        ax.plot(df['Fecha'], df[info['col']], marker='', linestyle='-', label=info['titulo'])
323
        ax.axhline(info['umbral'], color='red', linestyle='--', label=f'Alarma ({info["umbral"]:.0f} PPM)')
324
        
325
        ax.set_title(info['titulo'], fontsize=10)
326
        ax.set_ylabel('PPM', fontsize=8)
327
        
328
        # --- LÓGICA DE ESCALA OPTIMIZADA (AJUSTA EL EJE Y) ---
329
        max_val_data = df[info['col']].max()
330
        
331
        if max_val_data < info['umbral']:
332
            y_lim_max = max_val_data * 1.2 
333
            if y_lim_max < 5: y_lim_max = 5 
334
        else:
335
            y_lim_max = max_val_data * 1.2
336
            
337
        ax.set_ylim(0, y_lim_max)
338
        # -----------------------------------------------------
339
        
340
        ax.legend(fontsize=7, loc='upper left')
341
        
342
        # CORRECCIÓN CLAVE: Mostrar la marca de tiempo en todos los ejes
343
        ax.tick_params(axis='x', labelbottom=True) 
344
        for tick in ax.get_xticklabels():
345
            tick.set_rotation(30)
346
            tick.set_ha('right')
347

    
348
    # 5. Integrar el gráfico en la GUI
349
    for widget in historial_frame.winfo_children():
350
        widget.destroy()
351

    
352
    canvas = FigureCanvasTkAgg(fig, master=historial_frame)
353
    canvas_widget = canvas.get_tk_widget()
354
    canvas_widget.pack(fill=tk.BOTH, expand=True)
355
    canvas.draw()
356
    tk.Label(historial_frame, text="Gráficos generados de los tres sensores con escala optimizada.").pack(pady=5)
357

    
358

    
359
# ----------------------------------------------------
360
# BUCLE PRINCIPAL DE LECTURA (ACTUALIZA LA GUI)
361
# ----------------------------------------------------
362

    
363
def actualizar_sensores():
364
    """Bucle principal de lectura y actualización de la GUI."""
365
    global ALARMA_ACTIVA, MEDICION_ACTIVA, UMBRAL_CO, UMBRAL_CH4, UMBRAL_AIRE
366
    
367
    if not MEDICION_ACTIVA:
368
        return
369

    
370
    try:
371
        # LECTURA Y CÁLCULO DE SENSORES
372
        ppm_7 = MQ7_to_PPM(get_RS_resistance(grovepi.analogRead(MQ7_PIN)), R0_MQ7)
373
        ppm_4 = MQ4_to_PPM(get_RS_resistance(grovepi.analogRead(MQ4_PIN)), R0_MQ4)
374
        ppm_135 = MQ135_to_PPM(get_RS_resistance(grovepi.analogRead(MQ135_PIN)), R0_MQ135)
375
        
376
        # -------------------------------------------------------
377
        # LÓGICA DE ALARMA MULTI-SENSOR
378
        # -------------------------------------------------------
379
        alerta_co = ppm_7 >= UMBRAL_CO
380
        alerta_ch4 = ppm_4 >= UMBRAL_CH4
381
        alerta_aire = ppm_135 >= UMBRAL_AIRE
382
        
383
        if alerta_co or alerta_ch4 or alerta_aire:
384
            ALARMA_ACTIVA = True
385
            grovepi.digitalWrite(BUZZER_PIN, 1)
386
            estado_alarma_texto = "!!! ALERTA GENERAL !!!"
387
            color_alarma = "red"
388
            
389
            detalle = []
390
            if alerta_co: detalle.append("CO")
391
            if alerta_ch4: detalle.append("CH4")
392
            if alerta_aire: detalle.append("AIR")
393
            estado_alarma_texto += " (" + ", ".join(detalle) + ")"
394

    
395
        else:
396
            ALARMA_ACTIVA = False
397
            grovepi.digitalWrite(BUZZER_PIN, 0)
398
            estado_alarma_texto = "Monitor Activo"
399
            color_alarma = "green"
400
            
401
        # 2. GUARDAR DATOS 
402
        guardar_datos(ppm_7, ppm_4, ppm_135, estado_alarma_texto)
403
            
404
        # 3. ACTUALIZACIÓN DE LA GUI
405
        
406
        # Actualización de Medidores PPM 
407
        dibujar_medidor_ppm(canvas_mq7, ppm_7, UMBRAL_CO, MAX_CO_DISPLAY, "Monóxido Carbono (CO)")
408
        dibujar_medidor_ppm(canvas_mq4, ppm_4, UMBRAL_CH4, MAX_PPM_DISPLAY, "Metano (CH4)")
409
        dibujar_medidor_ppm(canvas_mq135, ppm_135, UMBRAL_AIRE, MAX_PPM_DISPLAY, "Calidad del Aire")
410

    
411
        estado_general_label.config(text=estado_alarma_texto, background=color_alarma)
412
        hora_var.set(datetime.now().strftime('%H:%M:%S'))
413

    
414
    except IOError:
415
        estado_general_label.config(text="Error de Conexión I/O", background="orange")
416
    except Exception as e:
417
        estado_general_label.config(text="Error", background="red")
418
        print(f"Error durante la lectura: {e}")
419
        detener_medicion()
420

    
421
    if MEDICION_ACTIVA:
422
        root.after(TIEMPO_LECTURA_MS, actualizar_sensores)
423

    
424

    
425
# ----------------------------------------------------
426
# CONFIGURACIÓN DE LA INTERFAZ GRÁFICA (TKINTER)
427
# ----------------------------------------------------
428

    
429
try:
430
    grovepi.pinMode(BUZZER_PIN, "OUTPUT")
431
except:
432
    pass 
433

    
434
root = tk.Tk()
435
root.title("KEL AviGasDetector")
436

    
437
# Establecer tamaño DINÁMICO (Escalable)
438
root.geometry("400x600") 
439
root.resizable(True, True) 
440

    
441
# --- Estilos Mejorados (Themed Tkinter) ---
442
style = ttk.Style()
443
style.theme_use('clam') 
444
style.configure('TNotebook.Tab', font=('Helvetica', 10, 'bold'))
445
style.configure('Red.TButton', foreground='white', background='#CC0000', font=('Helvetica', 10, 'bold'))
446
style.map('Red.TButton', background=[('active', '#FF3333')])
447
style.configure('Green.TButton', foreground='white', background='#006600', font=('Helvetica', 10, 'bold'))
448
style.map('Green.TButton', background=[('active', '#009900')])
449
style.configure('Dashboard.TButton', font=('Helvetica', 12), padding=10)
450

    
451
notebook = ttk.Notebook(root)
452
notebook.pack(pady=5, padx=5, expand=True, fill='both')
453

    
454
# --- Variables de control (Instanciadas globalmente) ---
455
mq7_var = tk.StringVar(root)
456
mq4_var = tk.StringVar(root)
457
mq135_var = tk.StringVar(root)
458
hora_var = tk.StringVar(root, value="")
459
entry_co_var = tk.StringVar(root, value=UMBRAL_CO_DEFECTO)
460
entry_ch4_var = tk.StringVar(root, value=UMBRAL_CH4_DEFECTO)
461
entry_aire_var = tk.StringVar(root, value=UMBRAL_AIRE_DEFECTO)
462

    
463

    
464
# -----------------------------------------------------
465
# --- PESTAÑA 1: INICIO/ACCESO ---
466
# -----------------------------------------------------
467
inicio_frame = ttk.Frame(notebook, padding="10")
468
notebook.add(inicio_frame, text='Inicio')
469

    
470
ttk.Label(inicio_frame, text="KEL AviGasDetector", font=("Helvetica", 20, "bold"), foreground="#007bff").pack(pady=30)
471
ttk.Label(inicio_frame, text="Monitor de Calidad del Aire", font=("Helvetica", 14)).pack(pady=5)
472
ttk.Label(inicio_frame, text="Seleccione una opción:", font=("Helvetica", 12)).pack(pady=20)
473

    
474
# Botones de Acción de Inicio
475
btn_iniciar_inicio = ttk.Button(inicio_frame, text="Empezar Medición", command=iniciar_medicion, style='Green.TButton')
476
btn_iniciar_inicio.pack(pady=10, ipadx=20, ipady=10)
477

    
478
ttk.Button(inicio_frame, text="VER HISTORIAL", command=abrir_historial, style='Dashboard.TButton').pack(pady=10, ipadx=20, ipady=5)
479
ttk.Button(inicio_frame, text="CONFIGURACIÓN", command=abrir_configuracion, style='Dashboard.TButton').pack(pady=10, ipadx=20, ipady=5)
480

    
481

    
482
# -----------------------------------------------------
483
# --- PESTAÑA 2: MEDICIONES (DASHBOARD 3 MEDIDORES) ---
484
# -----------------------------------------------------
485
mediciones_frame = ttk.Frame(notebook, padding="10")
486
notebook.add(mediciones_frame, text='Mediciones')
487

    
488
ttk.Label(mediciones_frame, text="Monitor PPM en Tiempo Real", font=("Helvetica", 16, "bold")).pack(pady=10)
489

    
490
# Contenedor para los 3 Canvas
491
medidores_grid = ttk.Frame(mediciones_frame)
492
medidores_grid.pack(pady=10, padx=10)
493

    
494
# Medidor MQ-7
495
canvas_mq7 = tk.Canvas(medidores_grid, width=200, height=120, bg="#F0F0F0")
496
canvas_mq7.grid(row=0, column=0, padx=5, pady=5)
497
dibujar_medidor_ppm(canvas_mq7, 0, UMBRAL_CO, MAX_CO_DISPLAY, "Monóxido Carbono (CO)")
498

    
499
# Medidor MQ-4
500
canvas_mq4 = tk.Canvas(medidores_grid, width=200, height=120, bg="#F0F0F0")
501
canvas_mq4.grid(row=0, column=1, padx=5, pady=5)
502
dibujar_medidor_ppm(canvas_mq4, 0, UMBRAL_CH4, MAX_PPM_DISPLAY, "Metano (CH4)")
503

    
504
# Medidor MQ-135 (Debajo de MQ-7)
505
canvas_mq135 = tk.Canvas(medidores_grid, width=200, height=120, bg="#F0F0F0")
506
canvas_mq135.grid(row=1, column=0, padx=5, pady=5)
507
dibujar_medidor_ppm(canvas_mq135, 0, UMBRAL_AIRE, MAX_PPM_DISPLAY, "Calidad del Aire")
508

    
509
# Etiqueta de Estado de Alarma Principal
510
estado_general_label = tk.Label(mediciones_frame, text="Presiona COMENZAR", font=("Helvetica", 14, "bold"), background="gray", foreground="white", width=30)
511
estado_general_label.pack(pady=15)
512

    
513
# Botón de Control
514
btn_iniciar_mediciones = ttk.Button(mediciones_frame, text="Empezar Medición", command=iniciar_medicion, style='Green.TButton')
515
btn_iniciar_mediciones.pack(pady=10)
516

    
517

    
518
# -----------------------------------------------------
519
# --- PESTAÑA 3: HISTORIAL Y GRÁFICOS ---
520
# -----------------------------------------------------
521
historial_frame = ttk.Frame(notebook, padding="10")
522
notebook.add(historial_frame, text='Historial y Gráficos')
523

    
524
ttk.Label(historial_frame, text="Visualización de Datos Históricos", font=("Helvetica", 14, "bold")).pack(pady=5)
525
ttk.Button(historial_frame, text="Generar TRES Gráficos por Sensor", command=generar_grafico).pack(pady=10)
526
ttk.Button(historial_frame, text="Limpiar Historial de Archivo", command=limpiar_historial, style='Red.TButton').pack(pady=5)
527

    
528

    
529
# -----------------------------------------------------
530
# --- PESTAÑA 4: OPCIONES (CONFIGURACIÓN) ---
531
# -----------------------------------------------------
532
opciones_frame = ttk.Frame(notebook, padding="10")
533
notebook.add(opciones_frame, text='Opciones de Alarma')
534

    
535
ttk.Label(opciones_frame, text="Configuración de Umbrales de Seguridad (PPM)", font=("Helvetica", 14, "bold")).pack(pady=10)
536

    
537
# Función auxiliar para crear filas de configuración
538
def crear_fila_umbral(parent, label_text, entry_var, unidad, defecto):
539
    frame = ttk.Frame(parent)
540
    frame.pack(fill='x', padx=5, pady=5)
541
    
542
    ttk.Label(frame, text=label_text, width=20, anchor='w').pack(side='left')
543
    ttk.Entry(frame, textvariable=entry_var, width=10).pack(side='left', padx=5)
544
    ttk.Label(frame, text=f"{unidad} (Defecto: {defecto:.0f} PPM)").pack(side='left')
545

    
546
# Filas de configuración
547
crear_fila_umbral(opciones_frame, "MQ-7 (CO):", entry_co_var, "PPM", UMBRAL_CO_DEFECTO)
548
crear_fila_umbral(opciones_frame, "MQ-4 (CH4):", entry_ch4_var, "PPM", UMBRAL_CH4_DEFECTO)
549
crear_fila_umbral(opciones_frame, "MQ-135 (Aire/CO2):", entry_aire_var, "PPM", UMBRAL_AIRE_DEFECTO)
550

    
551
# Botones de Acción
552
action_frame = ttk.Frame(opciones_frame)
553
action_frame.pack(pady=20)
554

    
555
ttk.Button(action_frame, text="GUARDAR Umbrales", command=aplicar_umbrales, style='Green.TButton').pack(side='left', padx=10)
556
ttk.Button(action_frame, text="Restaurar Defecto", command=reset_umbrales, style='Red.TButton').pack(side='left', padx=10)
557

    
558

    
559
# Botón de Salida Global
560
ttk.Button(root, text="SALIR (Cerrar Programa y Limpiar)", command=salir_limpiar, style='Red.TButton').pack(pady=10)
561

    
562
# Iniciar los valores por defecto en los Entry fields al inicio
563
entry_co_var.set(UMBRAL_CO_DEFECTO)
564
entry_ch4_var.set(UMBRAL_CH4_DEFECTO)
565
entry_aire_var.set(UMBRAL_AIRE_DEFECTO)
566

    
567
root.mainloop()