Files
Meraki_QRCodeSccaner/qr_detector.py
2025-02-04 10:42:57 -06:00

267 lines
9.6 KiB
Python

import os
import cv2
from pyzbar import pyzbar
import time
import logging
import sys
import yaml
import requests
from pathlib import Path
from typing import Optional, Tuple, List
from dataclasses import dataclass
from contextlib import contextmanager
import threading
from queue import Queue
from db_handler import DBHandler
@dataclass
class QRData:
"""Clase para almacenar información del código QR"""
data: str
code_type: str
coordinates: Tuple[int, int, int, int]
timestamp: float
class QRDetector:
def __init__(self, config_path: str = 'config.yaml'):
self.config = self.load_config(config_path)
self.setup_logging()
self.camera_id = self.config['camera']['id']
self.camera_name = self.config.get('camera', {}).get('name', 'NO_CAMERA_NAME')
self.min_delay = self.config['camera']['min_delay']
self.camera = None
self.last_code = None
self.last_time = 0
self.lotes = []
self.api_queue = Queue()
self.running = True
# Inicializar base de datos
self.db = DBHandler(self.config.get('database', {}).get('path', 'qr_codes.db'))
# Iniciar thread para procesar envíos a la API
self.api_thread = threading.Thread(target=self.api_worker)
self.api_thread.daemon = True
self.api_thread.start()
self.headless = self.config.get('headless', {}).get('enabled', False)
# Crear directorio para imágenes si es necesario
if self.headless and self.config['headless'].get('save_images', False):
os.makedirs(self.config['headless']['image_output_dir'], exist_ok=True)
# Verificar configuración de API
if 'api' in self.config:
self.logger.info(f"Configuración API cargada:")
self.logger.info(f"URL: {self.config['api']['url']}")
self.logger.info(f"Batch size: {self.config['api'].get('batch_size', 1)}")
else:
self.logger.error("No se encontró configuración de API")
def load_config(self, config_path: str) -> dict:
"""Carga la configuración desde el archivo YAML"""
try:
with open(config_path, 'r') as file:
return yaml.safe_load(file)
except Exception as e:
self.logger.error(f"Error cargando configuración: {e}")
sys.exit(1)
def setup_logging(self):
"""Configura el sistema de logging"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('qr_detector.log')
]
)
self.logger = logging.getLogger(__name__)
def api_worker(self):
"""Worker thread para procesar envíos a la API"""
while self.running:
try:
lotes = self.api_queue.get(timeout=1)
if lotes:
self._send_to_api(lotes)
self.api_queue.task_done()
except:
continue
def _send_to_api(self, lotes):
"""Envía los lotes a la API (método interno)"""
try:
api_url = self.config['api']['url']
headers = {
'Content-Type': 'application/json',
'x-api-key': self.config['api']['api_key']
}
payload = {
"lotes": lotes
}
self.logger.info(f"Enviando lotes a API: {lotes}")
response = requests.post(api_url, json=payload, headers=headers, timeout=5)
if response.status_code == 200:
self.logger.info(f"Lotes enviados exitosamente")
else:
self.logger.error(f"Error enviando lotes: {response.status_code}")
except Exception as e:
self.logger.error(f"Error en envío a API: {e}")
def send_to_api(self):
"""Encola los lotes para envío asíncrono"""
if not self.lotes:
return
# Copiar los lotes actuales y limpiar la lista
lotes_to_send = self.lotes.copy()
self.lotes = []
# Encolar para envío asíncrono
self.api_queue.put(lotes_to_send)
@contextmanager
def initialize_camera(self):
"""Inicializa la cámara"""
try:
self.camera = cv2.VideoCapture(self.camera_id)
if not self.camera.isOpened():
self.logger.error("No se pudo abrir la cámara")
sys.exit(1)
yield self.camera
finally:
if self.camera:
self.camera.release()
cv2.destroyAllWindows()
self.send_to_api() # Enviar lotes pendientes al cerrar
self.logger.info("Recursos liberados")
def process_frame(self, frame) -> List[QRData]:
"""Procesa un frame y retorna los códigos QR detectados"""
try:
qr_codes = pyzbar.decode(frame)
current_time = time.time()
results = []
for qr_code in qr_codes:
try:
data = qr_code.data.decode('utf-8')
code_type = qr_code.type
coordinates = qr_code.rect
if (data != self.last_code or
(current_time - self.last_time) > self.min_delay):
# Verificar si el QR ya existe en la base de datos local
if self.db.check_qr_exists(data):
self.logger.info(f"QR ya existe en la base de datos: {data}")
continue
# Guardar en la base de datos local
if self.db.save_qr(data,self.camera_name):
self.logger.info(f"QR guardado en base de datos: {data}")
qr_data = QRData(
data=data,
code_type=code_type,
coordinates=coordinates,
timestamp=current_time
)
results.append(qr_data)
# Añadir a lotes para envío a API solo si es nuevo
if data not in self.lotes:
self.lotes.append(data)
self.logger.info(f"Añadido a lotes para API. Total: {len(self.lotes)}")
self.last_code = data
self.last_time = current_time
except Exception as e:
self.logger.error(f"Error procesando QR individual: {e}")
return results
except Exception as e:
self.logger.error(f"Error procesando frame: {e}")
return []
def draw_qr_info(self, frame, qr_data: QRData):
"""Dibuja la información del QR en el frame"""
try:
x, y, w, h = qr_data.coordinates
# Dibujar rectángulo
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
# Mostrar información
display_text = f"{qr_data.code_type}: {qr_data.data}"
cv2.putText(
frame,
display_text,
(x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 255, 0),
2
)
except Exception as e:
self.logger.error(f"Error dibujando información: {e}")
def run(self):
"""Método principal para ejecutar el detector"""
self.logger.info("Iniciando detector de códigos QR...")
batch_size = self.config['api'].get('batch_size', 1)
self.logger.info(f"Batch size configurado: {batch_size}")
with self.initialize_camera() as camera:
while True:
try:
success, frame = camera.read()
if not success:
self.logger.error("Error capturando frame")
break
qr_codes = self.process_frame(frame)
for qr_data in qr_codes:
if self.headless:
self.save_frame_with_qr(frame, qr_data)
else:
self.draw_qr_info(frame, qr_data)
cv2.imshow('QR Detector', frame)
if not self.headless:
if cv2.waitKey(1) & 0xFF == ord('q'):
self.running = False
if self.lotes:
self.logger.info(f"Enviando {len(self.lotes)} lotes pendientes...")
self.send_to_api()
self.api_queue.join()
break
# Verificar y enviar lotes
if len(self.lotes) >= batch_size:
self.send_to_api()
except Exception as e:
self.logger.error(f"Error en el bucle principal: {e}")
continue
def main():
detector = QRDetector()
detector.run()
if __name__ == "__main__":
main()