diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a958c97..0000000 --- a/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*.pyo -*.pyd -.Python - -# Virtual environment -venv/ -env/ -.venv/ - -# Django -*.sqlite3 -/media/ -/staticfiles/ - -# Environment variables -.env -.env.local - -# Claude Code workspace -.claude/ -CLAUDE.md - -# IDE -.vscode/ -.idea/ -*.iml - -# OS -.DS_Store -Thumbs.db - -# Logs -*.log - -# Grabaciones y capturas del monitor de boxes -Escritorio_Boxes/sesiones/ -Escritorio_Boxes/capturas/ diff --git a/Escritorio_Boxes/diseño.py b/Escritorio_Boxes/diseño.py deleted file mode 100644 index 3b47a9d..0000000 --- a/Escritorio_Boxes/diseño.py +++ /dev/null @@ -1,80 +0,0 @@ -import socket -import time -import math -import random - -# --- CONFIGURACIÓN DE RED --- -UDP_IP = "127.0.0.1" # Enviamos a nuestro propio PC (Localhost) -UDP_PORT = 4210 - -# Formato del paquete: "clave=valor" separados por ';'. El monitor tolera que -# falte cualquier canal, así que se pueden comentar líneas del diccionario de -# abajo para simular canales que el firmware todavía no envía. -FORMATO_CLAVE_VALOR = True - -sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - -print("--- SIMULADOR G26 INICIADO ---") -print(f"Enviando a {UDP_IP}:{UDP_PORT}") -print("Formato:", "clave=valor" if FORMATO_CLAVE_VALOR else "posicional (antiguo)") - -t = 0.0 - -while True: - # El acelerador manda: marca el régimen, la carga y la mezcla - tps = max(0.0, 100 * abs(math.sin(t / 3.0)) - 5) - carga = tps / 100.0 - - # Régimen siguiendo al acelerador, con algo de inercia - rpm = 1200 + 12800 * (carga ** 0.7) + random.uniform(-120, 120) - - # Velocidad: correlada con el régimen (modelo simple para el simulador) - velocidad = 15 + 105 * (carga ** 0.8) + random.uniform(-1.5, 1.5) - - # Freno delantero: presión (bar). Frena al levantar el pie del acelerador - freno_del = max(0.0, 45 * max(0.0, min(1.0, (6 - tps) / 6)) + random.uniform(-0.4, 0.4)) - - # Temperaturas: suben poco a poco y responden a la carga - ect = 88 + 6 * math.sin(t / 20.0) + 4 * carga + random.uniform(-0.3, 0.3) - taceite = 98 + 8 * math.sin(t / 25.0) + 6 * carga + random.uniform(-0.4, 0.4) - - # MAP: motor atmosférico -> depresión en ralentí, cerca de atmosférica a fondo - mapa = 28 + 72 * carga + random.uniform(-1.5, 1.5) - - # Presión de combustible: cae ligeramente al pedir caudal - pcomb = 3.8 - 0.35 * carga + random.uniform(-0.04, 0.04) - - # Presión de aceite: sube con el régimen (regla aproximada de 1 bar/1000 rpm) - paceite = max(0.8, min(6.5, rpm / 2200.0)) + random.uniform(-0.05, 0.05) - - # Lambda: la ECU enriquece en carga (objetivo ~0,88) y ronda 1,00 en crucero - lambda_obj = 1.00 - 0.13 * carga - lambda_val = lambda_obj + random.uniform(-0.025, 0.025) - - # Batería: cargando con el motor en marcha - vbatt = 13.9 + 0.25 * math.sin(t / 7.0) - 0.3 * carga - - if FORMATO_CLAVE_VALOR: - campos = { - 'ect': f'{ect:.1f}', - 'rpm': f'{int(rpm)}', - 'vbatt': f'{vbatt:.2f}', - 'pcomb': f'{pcomb:.2f}', - 'taceite': f'{taceite:.1f}', - 'paceite': f'{paceite:.2f}', - 'map': f'{mapa:.0f}', - 'lambda': f'{lambda_val:.3f}', - 'lambda_obj': f'{lambda_obj:.3f}', - 'tps': f'{tps:.0f}', - 'velocidad': f'{velocidad:.0f}', - 'freno_del': f'{freno_del:.1f}', - } - mensaje = ';'.join(f'{clave}={valor}' for clave, valor in campos.items()) - else: - # Formato antiguo de tres campos, el que emite el firmware actual - mensaje = f"{ect:.1f}|{int(rpm)}|{vbatt:.1f}" - - sock.sendto(mensaje.encode('utf-8'), (UDP_IP, UDP_PORT)) - - t += 0.1 - time.sleep(0.05) # 20 paquetes por segundo, como la ESP32 diff --git a/Escritorio_Boxes/logo_gades.png b/Escritorio_Boxes/logo_gades.png deleted file mode 100644 index 36e0782..0000000 Binary files a/Escritorio_Boxes/logo_gades.png and /dev/null differ diff --git a/Escritorio_Boxes/monitor.py b/Escritorio_Boxes/monitor.py deleted file mode 100644 index 9db8b99..0000000 --- a/Escritorio_Boxes/monitor.py +++ /dev/null @@ -1,898 +0,0 @@ -import socket -import os -import csv -import matplotlib.pyplot as plt -import matplotlib.animation as animation -from matplotlib.gridspec import GridSpec -from matplotlib.patches import Rectangle, Polygon -from matplotlib.lines import Line2D -from matplotlib.colors import to_rgb -from collections import deque -import time -import numpy as np -from datetime import datetime - -# --- CONFIGURACIÓN DE RED --- -UDP_IP = "0.0.0.0" # Escuchamos en Todas las interfaces posibles (WiFi, Ethernet...) -UDP_PORT = 4210 # Mismo puerto que usamos para la ESP32 -TIMEOUT_SEG = 1.5 # Para ver si existe desconexión - -# --- CONFIGURACIÓN DE DATOS --- -FRECUENCIA_HZ = 20 # Frecuencia a la que emite la ESP32 (un paquete cada 50 ms) -VENTANA_SEG = 10 # Segundos de historia visibles en las gráficas -MAX_PUNTOS = FRECUENCIA_HZ * VENTANA_SEG - -# --- FORMATO DEL PAQUETE UDP --- -# Se admiten dos formatos y se distinguen solos: -# -# 1. "87.3|9200|14.2" -> ECT | RPM | BATERÍA -# -# 2. Nuevo (clave-valor, para cuando el firmware envíe el resto de canales): -# "ect=87.3;rpm=9200;vbatt=14.2;velocidad=64;tps=45;freno_del=8.5; -# pcomb=3.6;taceite=104;paceite=4.2;map=98;lambda=0.88;lambda_obj=0.88" -CLAVES_LEGADO = ('ect', 'rpm', 'vbatt') - -# --- PALETA (tomada del logo oficial del equipo) --- -FONDO = '#080D1A' # Navy del logo llevado casi a negro -PANEL = '#0E1526' # Fondo de los paneles -GRID = '#1C2946' # Rejilla y separadores -TXT = '#E9EEF8' # Texto principal -TXT_DIM = '#7C8AA8' # Etiquetas secundarias -AZUL = '#6E9BE0' # Azul del logo, aclarado para fondo oscuro -VERDE = '#35D07F' # Semáforo: correcto -AMBAR = '#FFB627' # Semáforo: precaución -ROJO = '#FF4D4D' # Semáforo: crítico -CIAN = '#4FC3F7' # Motor frío -APAGADO = '#18213A' # Segmento / relleno inactivo - -# --- RÉGIMEN DE MOTOR --- -MAX_RPM = 13000 # Fondo de escala del indicador -RPM_CORTE = 12000 # Zona roja / corte de inyección - -# --- PEDALES --- -VENTANA_PEDALES_SEG = 7 # Segundos visibles en la traza de pedales -PUNTOS_PEDALES = FRECUENCIA_HZ * VENTANA_PEDALES_SEG -# El sensor de freno es de PRESIÓN (bar). Este es el valor que se dibuja como el -# 100 % de la traza. AJUSTAR cuando se mida en pista la frenada más fuerte. -FRENO_PRESION_MAX = 50.0 - -# --- DEFINICIÓN DE CANALES --- -# Cada canal declara su rango visible, sus zonas de color y una referencia -# opcional que se dibuja como línea en la gráfica. Las 'zonas' son pares -# (límite superior, color): se recorre en orden y gana la primera que supera -# al valor. AJUSTAR ESTOS UMBRALES A VUESTRO MOTOR. -CANALES = { - 'ect': dict( - etiqueta='ECT', descripcion='TEMP. REFRIGERANTE', unidad='°C', - vmin=0, vmax=130, decimales=1, referencia=None, - zonas=[(65, CIAN), (95, VERDE), (105, AMBAR), (float('inf'), ROJO)]), - 'taceite': dict( - etiqueta='T. ACEITE', descripcion='TEMP. DE ACEITE', unidad='°C', - vmin=0, vmax=160, decimales=1, referencia=None, - zonas=[(60, CIAN), (125, VERDE), (135, AMBAR), (float('inf'), ROJO)]), - 'pcomb': dict( - etiqueta='P. COMBUSTIBLE', descripcion='PRESIÓN COMBUSTIBLE', unidad='bar', - vmin=0, vmax=6, decimales=2, referencia=3.5, - zonas=[(2.5, ROJO), (3.0, AMBAR), (4.5, VERDE), (float('inf'), AMBAR)]), - 'paceite': dict( - etiqueta='P. ACEITE', descripcion='PRESIÓN DE ACEITE', unidad='bar', - vmin=0, vmax=8, decimales=2, referencia=None, - zonas=[(1.0, ROJO), (2.0, AMBAR), (6.5, VERDE), (float('inf'), AMBAR)]), - 'lambda': dict( - etiqueta='LAMBDA', descripcion='MEZCLA (λ)', unidad='', - vmin=0.70, vmax=1.30, decimales=2, referencia=1.00, - zonas=[(0.75, ROJO), (0.80, AMBAR), (1.02, VERDE), - (1.08, AMBAR), (float('inf'), ROJO)]), - 'map': dict( - etiqueta='MAP', descripcion='PRESIÓN DE ADMISIÓN', unidad='kPa', - vmin=0, vmax=120, decimales=0, referencia=101.3, # Presión atmosférica - zonas=[(float('inf'), AZUL)]), # Es carga, no alarma - 'vbatt': dict( - etiqueta='BATERÍA', descripcion='TENSIÓN DE BATERÍA', unidad='V', - vmin=0, vmax=16, decimales=1, referencia=None, - zonas=[(11.8, ROJO), (12.4, AMBAR), (14.8, VERDE), (float('inf'), ROJO)]), - 'tps': dict( - etiqueta='TPS', descripcion='ACELERADOR', unidad='%', - vmin=0, vmax=100, decimales=0, referencia=None, - zonas=[(float('inf'), AZUL)]), - # Frenos: sensores de PRESIÓN. Se guardan en bar (real) y en la traza se - # normalizan a 0-100 % contra FRENO_PRESION_MAX. Solo el delantero está - # instalado; el trasero queda declarado a la espera de montarse. - 'freno_del': dict( - etiqueta='FRENO DEL.', descripcion='PRESIÓN FRENO DELANTERO', unidad='bar', - vmin=0, vmax=FRENO_PRESION_MAX, decimales=1, referencia=None, - zonas=[(float('inf'), ROJO)]), - 'freno_tra': dict( - etiqueta='FRENO TRA.', descripcion='PRESIÓN FRENO TRASERO', unidad='bar', - vmin=0, vmax=FRENO_PRESION_MAX, decimales=1, referencia=None, - zonas=[(float('inf'), ROJO)]), - 'velocidad': dict( - etiqueta='VELOCIDAD', descripcion='VELOCIDAD', unidad='km/h', - vmin=0, vmax=160, decimales=0, referencia=None, - zonas=[(float('inf'), AZUL)]), - 'rpm': dict( - etiqueta='RPM', descripcion='RÉGIMEN DE MOTOR', unidad='', - vmin=0, vmax=MAX_RPM, decimales=0, referencia=None, - zonas=[(RPM_CORTE, VERDE), (float('inf'), ROJO)]), -} - -# Canales que ocupan las tarjetas inferiores, en orden. Su posición es además la -# tecla que lleva ese canal a la gráfica grande (1 = primera tarjeta, etc.). -TARJETAS = ['ect', 'taceite', 'paceite', 'pcomb', 'lambda', 'map', 'vbatt'] -CANAL_FOCO_INICIAL = 'ect' - -# Lambda se colorea por desviación respecto al objetivo que manda la ECU, no -# por umbrales fijos: mezcla pobre funde pistones, rica solo pierde potencia -LAMBDA_POBRE_CRITICO = 0.06 -LAMBDA_POBRE_AVISO = 0.03 -LAMBDA_RICA_AVISO = -0.08 - -RUTA_BASE = os.path.dirname(os.path.abspath(__file__)) -RUTA_LOGO = os.path.join(RUTA_BASE, 'logo_gades.png') -RUTA_SESIONES = os.path.join(RUTA_BASE, 'sesiones') -RUTA_CAPTURAS = os.path.join(RUTA_BASE, 'capturas') - -# --- ESTADO --- -historial = {clave: deque([np.nan] * MAX_PUNTOS, maxlen=MAX_PUNTOS) for clave in CANALES} -ultima_lectura = {} # Último valor recibido de cada canal -maximos = {} # Máximo de sesión por canal -minimos = {} # Mínimo de sesión por canal -canal_foco = CANAL_FOCO_INICIAL - -ultimo_tiempo_dato = 0.0 -conectado = False -inicio_sesion = None -paquetes_ok = 0 -paquetes_error = 0 -sellos_tiempo = deque(maxlen=FRECUENCIA_HZ * 3) - -# Grabación a CSV -grabando = False -inicio_grabacion = None -fichero_csv = None -escritor_csv = None -nombre_grabacion = '' -muestras_grabadas = 0 - -# Pie de pantalla: estado permanente + avisos temporales que lo tapan unos segundos -texto_pie = '' -aviso_pie = '' -aviso_hasta = 0.0 - -# Orden de las columnas del CSV de sesión -COLUMNAS_CSV = ['ect', 'rpm', 'velocidad', 'vbatt', 'tps', 'freno_del', 'freno_tra', - 'pcomb', 'taceite', 'paceite', 'map', 'lambda'] - -sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -sock.bind((UDP_IP, UDP_PORT)) -sock.setblocking(False) - - -# --- LÓGICA DE DATOS --- -def parsear_mensaje(msg): - """Convierte el paquete UDP en un diccionario canal -> valor. - - Acepta el formato clave-valor y el posicional antiguo, para que el monitor - siga funcionando con el firmware que ya está flasheado en el coche.""" - if '=' in msg: - lectura = {} - for par in msg.replace(';', '|').split('|'): - clave, sep, valor = par.partition('=') - if not sep: - continue - try: - lectura[clave.strip().lower()] = float(valor) - except ValueError: - continue # Una clave ilegible no invalida el resto del paquete - if not lectura: - raise ValueError('paquete clave-valor sin ningún campo legible') - return lectura - - partes = msg.split('|') - if len(partes) != len(CLAVES_LEGADO): - raise ValueError(f'se esperaban {len(CLAVES_LEGADO)} campos, llegaron {len(partes)}') - return {clave: float(valor) for clave, valor in zip(CLAVES_LEGADO, partes)} - - -def color_de(clave, valor): - """Color semáforo de un valor según las zonas declaradas para su canal.""" - if valor is None or (isinstance(valor, float) and np.isnan(valor)): - return TXT_DIM - if clave == 'lambda': - return color_lambda(valor, ultima_lectura.get('lambda_obj')) - for limite, color in CANALES[clave]['zonas']: - if valor < limite: - return color - return TXT - - -def color_lambda(valor, objetivo): - """Mezcla pobre es peligrosa (funde pistones); rica solo desperdicia.""" - if not objetivo or objetivo <= 0: - objetivo = CANALES['lambda']['referencia'] - desviacion = valor - objetivo - if desviacion >= LAMBDA_POBRE_CRITICO: - return ROJO - if desviacion >= LAMBDA_POBRE_AVISO: - return AMBAR - if desviacion <= LAMBDA_RICA_AVISO: - return AMBAR - return VERDE - - -def referencia_de(clave): - """Valor de referencia del canal. En lambda lo manda la propia ECU.""" - if clave == 'lambda': - return ultima_lectura.get('lambda_obj') or CANALES['lambda']['referencia'] - return CANALES[clave]['referencia'] - - -def formatear(clave, valor): - if valor is None or (isinstance(valor, float) and np.isnan(valor)): - return '--' - decimales = CANALES[clave]['decimales'] - if decimales == 0: - return f'{int(round(valor)):,}'.replace(',', '.') - return f'{valor:.{decimales}f}' - - -def valor_csv(clave): - """Valor para el fichero de sesión: sin separador de millares, que rompería - el CSV, y con los decimales propios del canal (RPM y velocidad son enteros).""" - valor = ultima_lectura.get(clave) - if valor is None: - return '' - decimales = CANALES[clave]['decimales'] if clave in CANALES else 0 - return f'{valor:.{decimales}f}' - - -def formato_tiempo(segundos): - if segundos is None: - return '--:--' - segundos = int(segundos) - return f'{segundos // 60:02d}:{segundos % 60:02d}' - - -def cargar_logo(ruta): - """Devuelve el logo con los azules oscuros aclarados para que se lea sobre - fondo oscuro. El naranja corporativo se mantiene intacto.""" - if not os.path.isfile(ruta): - return None - img = plt.imread(ruta) - if img.dtype == np.uint8: - img = img.astype(float) / 255.0 - else: - img = img.astype(float).copy() - if img.ndim != 3 or img.shape[2] < 3: - return None - if img.shape[2] == 3: - img = np.dstack([img, np.ones(img.shape[:2])]) - - rgb = img[:, :, :3] - luminancia = rgb @ np.array([0.299, 0.587, 0.114]) - oscuro = luminancia < 0.30 - medio = (luminancia >= 0.30) & (luminancia < 0.58) & (rgb[:, :, 2] > rgb[:, :, 0]) - img[oscuro, :3] = to_rgb(TXT) - img[medio, :3] = to_rgb(AZUL) - return img - - -# --- ESTILO GLOBAL --- -plt.rcParams['toolbar'] = 'None' -# Sin barra de herramientas matplotlib ignora su propio atajo de guardado, así que -# la captura la gestionamos nosotros. Y 'r' viene asignada de fábrica a "reiniciar -# vista": se la quitamos para que sea inequívocamente la tecla de grabar. -plt.rcParams['keymap.save'] = [] -plt.rcParams['keymap.home'] = [t for t in plt.rcParams['keymap.home'] if t != 'r'] -plt.rcParams['font.family'] = 'sans-serif' -plt.rcParams['font.sans-serif'] = ['Bahnschrift', 'Segoe UI', 'Franklin Gothic Medium', 'DejaVu Sans'] -plt.rcParams['figure.facecolor'] = FONDO -plt.rcParams['text.color'] = TXT -plt.rcParams['axes.edgecolor'] = GRID -plt.rcParams['xtick.color'] = TXT_DIM -plt.rcParams['ytick.color'] = TXT_DIM - -fig = plt.figure(figsize=(16, 9)) -fig.canvas.manager.set_window_title('G26 Telemetry - Formula Gades') - - -def preparar_panel(ax, color_acento=None, fondo=PANEL): - """Fondo de panel + barra de acento a la izquierda, el mismo lenguaje visual - que las tarjetas de la plataforma web.""" - ax.set_facecolor(fondo) - for spine in ax.spines.values(): - spine.set_visible(False) - ax.set_xticks([]) - ax.set_yticks([]) - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - if color_acento is None: - return None - acento = Rectangle((0, 0), 0.011, 1, transform=ax.transAxes, - facecolor=color_acento, zorder=5, clip_on=False) - ax.add_patch(acento) - return acento - - -# --- CABECERA --- -logo = cargar_logo(RUTA_LOGO) -if logo is not None: - alto_logo = 0.42 / 9.0 - ancho_logo = (0.42 * (logo.shape[1] / logo.shape[0])) / 16.0 - ax_logo = fig.add_axes([0.035, 0.9315, ancho_logo, alto_logo]) - ax_logo.imshow(logo) - ax_logo.set_axis_off() - x_titulo = 0.035 + ancho_logo + 0.018 -else: - x_titulo = 0.035 - -fig.text(x_titulo, 0.962, 'TELEMETRÍA G26', fontsize=15, fontweight='bold', color=TXT, va='center') -fig.text(x_titulo, 0.937, 'MURO DE BOXES · UNIVERSIDAD DE CÁDIZ', fontsize=8, color=TXT_DIM, va='center') - -txt_reloj = fig.text(0.975, 0.962, '--:--:--', fontsize=19, color=TXT, ha='right', va='center') - - -def crear_led(x, y, color): - """Los pilotos de estado van como marcadores, no como carácter: la tipografía - condensada no incluye el glifo del círculo.""" - led = Line2D([x], [y], marker='o', markersize=8, color=color, - transform=fig.transFigure, figure=fig) - fig.add_artist(led) - return led - - -led_estado = crear_led(0.7035, 0.937, TXT_DIM) -txt_estado = fig.text(0.713, 0.937, 'SIN SEÑAL', fontsize=10, color=TXT_DIM, va='center') -txt_hz = fig.text(0.830, 0.937, '-- Hz', fontsize=10, color=TXT_DIM, va='center') -led_rec = crear_led(0.9015, 0.937, APAGADO) -txt_rec = fig.text(0.911, 0.937, 'SIN GRABAR', fontsize=10, color=TXT_DIM, va='center') - -# Banda de alarma: con tantos canales ya no se pueden vigilar todos a la vez, -# así que la alarma viene a buscarte en lugar de esperar a que mires la casilla -txt_alarma = fig.text(0.50, 0.952, '', fontsize=12, fontweight='bold', color=TXT, - ha='center', va='center', zorder=10, - bbox=dict(facecolor=ROJO, edgecolor='none', boxstyle='square,pad=0.45')) -txt_alarma.set_visible(False) - -fig.add_artist(Line2D([0.035, 0.975], [0.916, 0.916], color=GRID, lw=1.2, - transform=fig.transFigure)) - -txt_sesion = fig.text(0.035, 0.017, '', fontsize=8.5, color=TXT_DIM, va='center') -fig.text(0.50, 0.017, 'R grabar S captura 1-7 gráfica F pantalla completa Q salir', - fontsize=8.5, color=TXT_DIM, ha='center', va='center') -txt_fichero = fig.text(0.975, 0.017, '', fontsize=8.5, color=TXT_DIM, ha='right', va='center') - -# --- BANDA A: LO QUE HACE EL PILOTO (RPM + VELOCIDAD + PEDALES) --- -gs_a = GridSpec(1, 3, width_ratios=[2.6, 0.8, 1.7], - left=0.035, right=0.975, top=0.893, bottom=0.712, wspace=0.030) - -# A1. Luces de cambio -ax_rpm = fig.add_subplot(gs_a[0, 0]) -preparar_panel(ax_rpm) -ax_rpm.set_xlim(0, 100) - -N_SEGMENTOS = 22 -ANCHO_TIRA = 78.0 -paso = ANCHO_TIRA / N_SEGMENTOS -segmentos = [] -colores_segmento = [] -for i in range(N_SEGMENTOS): - fraccion = (i + 1) / N_SEGMENTOS - if fraccion <= 0.55: - color = VERDE - elif fraccion <= RPM_CORTE / MAX_RPM: - color = AMBAR - else: - color = ROJO - colores_segmento.append(color) - seg = Rectangle((2 + i * paso, 0.34), paso * 0.76, 0.40, facecolor=APAGADO) - ax_rpm.add_patch(seg) - segmentos.append(seg) - -ax_rpm.text(2, 0.26, '0', fontsize=7.5, color=TXT_DIM, va='top') -ax_rpm.text(2 + ANCHO_TIRA * (RPM_CORTE / MAX_RPM), 0.26, f'{RPM_CORTE // 1000}.000', - fontsize=7.5, color=ROJO, va='top', ha='center') -ax_rpm.text(2 + ANCHO_TIRA, 0.26, f'{MAX_RPM // 1000}.000', fontsize=7.5, color=TXT_DIM, - va='top', ha='right') -ax_rpm.text(2, 0.88, 'RPM', fontsize=9, color=TXT_DIM, va='center') -txt_rpm = ax_rpm.text(98, 0.55, '--', fontsize=30, fontweight='bold', color=TXT_DIM, - ha='right', va='center') - -# A2. Velocidad: número grande, hereda el hueco glanceable que dejó la marcha -ax_vel = fig.add_subplot(gs_a[0, 1]) -preparar_panel(ax_vel, AZUL) -ax_vel.text(0.5, 0.86, 'VELOCIDAD', fontsize=9, color=TXT_DIM, ha='center', va='center') -txt_vel = ax_vel.text(0.5, 0.45, '--', fontsize=54, fontweight='bold', color=TXT_DIM, - ha='center', va='center') -ax_vel.text(0.5, 0.13, 'km/h', fontsize=12, color=TXT_DIM, ha='center', va='center') - -# A3. Traza de pedales: acelerador (verde) y freno (rojo) oscilando de 0 a 100 %, -# como en la telemetría real. El freno es presión (bar) normalizada a % en pantalla. -ax_ped = fig.add_subplot(gs_a[0, 2]) -ax_ped.set_facecolor(PANEL) -for lado in ('top', 'right'): - ax_ped.spines[lado].set_visible(False) -for lado in ('bottom', 'left'): - ax_ped.spines[lado].set_color(GRID) -ax_ped.set_xlim(0, PUNTOS_PEDALES) -ax_ped.set_ylim(0, 105) -ax_ped.set_yticks([0, 50, 100]) -ax_ped.set_yticklabels(['0', '50', '100'], fontsize=7) -ax_ped.set_xticks([0, PUNTOS_PEDALES / 2, PUNTOS_PEDALES]) -ax_ped.set_xticklabels([f'-{VENTANA_PEDALES_SEG:g} s'.replace('.', ','), - f'-{VENTANA_PEDALES_SEG / 2:g} s'.replace('.', ','), 'ahora'], fontsize=7) -ax_ped.tick_params(length=0, labelsize=7.5) -ax_ped.grid(True, color=GRID, alpha=0.5, linestyle='--', lw=0.6, zorder=1) -ax_ped.text(PUNTOS_PEDALES * 0.015, 99, 'PEDALES', fontsize=9, color=TXT_DIM, va='top', zorder=5) - -fill_tps = Polygon([[0, 0], [0, 0]], closed=True, facecolor=VERDE, alpha=0.22, - edgecolor='none', zorder=2) -fill_freno = Polygon([[0, 0], [0, 0]], closed=True, facecolor=ROJO, alpha=0.20, - edgecolor='none', zorder=2) -ax_ped.add_patch(fill_tps) -ax_ped.add_patch(fill_freno) -line_tps, = ax_ped.plot([], [], color=VERDE, lw=2, zorder=4, solid_capstyle='round') -line_freno, = ax_ped.plot([], [], color=ROJO, lw=2, zorder=3, solid_capstyle='round') -_caja_ped = dict(facecolor=PANEL, alpha=0.65, edgecolor='none', boxstyle='square,pad=0.2') -txt_ped_tps = ax_ped.text(PUNTOS_PEDALES * 0.985, 80, 'ACEL --', fontsize=10.5, - fontweight='bold', color=VERDE, ha='right', va='center', - zorder=6, bbox=_caja_ped) -txt_ped_freno = ax_ped.text(PUNTOS_PEDALES * 0.985, 62, 'FRENO --', fontsize=10.5, - fontweight='bold', color=ROJO, ha='right', va='center', - zorder=6, bbox=_caja_ped) - - -def area_pedales(xs, ys): - """Vértices del polígono relleno bajo una curva de pedal, saltándose los NaN - (que aparecen al desconectar), para que el relleno no se rompa.""" - m = np.isfinite(ys) - if not m.any(): - return [[0, 0], [0, 0]] - xf, yf = xs[m], ys[m] - return [(xf[0], 0)] + list(zip(xf, yf)) + [(xf[-1], 0)] - -# --- BANDA B: CANAL CON FOCO (número grande + gráfica) --- -gs_b = GridSpec(1, 2, width_ratios=[1, 3.3], - left=0.035, right=0.975, top=0.678, bottom=0.318, wspace=0.09) - -ax_foco_num = fig.add_subplot(gs_b[0, 0]) -acento_foco = preparar_panel(ax_foco_num, TXT_DIM) -txt_foco_etiqueta = ax_foco_num.text(0.5, 0.90, '', fontsize=13, fontweight='bold', - color=TXT_DIM, ha='center', va='center') -txt_foco_desc = ax_foco_num.text(0.5, 0.835, '', fontsize=7.5, color=TXT_DIM, - ha='center', va='center') -txt_foco_valor = ax_foco_num.text(0.5, 0.55, '--', fontsize=76, fontweight='bold', - color=TXT_DIM, ha='center', va='center') -txt_foco_unidad = ax_foco_num.text(0.5, 0.345, '', fontsize=18, color=TXT_DIM, - ha='center', va='center') -txt_foco_estado = ax_foco_num.text(0.5, 0.16, 'SIN SEÑAL', fontsize=11, fontweight='bold', - color=TXT_DIM, ha='center', va='center') - -ax_foco = fig.add_subplot(gs_b[0, 1]) -ax_foco.set_facecolor(PANEL) -for lado in ('top', 'right'): - ax_foco.spines[lado].set_visible(False) -for lado in ('bottom', 'left'): - ax_foco.spines[lado].set_color(GRID) -ax_foco.set_xlim(0, MAX_PUNTOS) -ax_foco.set_xticks([0, MAX_PUNTOS * 0.25, MAX_PUNTOS * 0.5, MAX_PUNTOS * 0.75, MAX_PUNTOS]) -ax_foco.set_xticklabels([f'-{VENTANA_SEG:g} s'.replace('.', ','), - f'-{VENTANA_SEG * 0.75:g} s'.replace('.', ','), - f'-{VENTANA_SEG * 0.5:g} s'.replace('.', ','), - f'-{VENTANA_SEG * 0.25:g} s'.replace('.', ','), 'ahora'], - fontsize=8) -ax_foco.grid(True, color=GRID, alpha=0.55, linestyle='--', lw=0.7, zorder=1) -ax_foco.tick_params(length=0, labelsize=9) -line_foco, = ax_foco.plot([], [], color=TXT, lw=2.2, zorder=3, solid_capstyle='round') -punto_foco, = ax_foco.plot([], [], marker='o', markersize=7, color=TXT, zorder=4) -adornos_foco = [] # Bandas, líneas de umbral y etiquetas: se rehacen al cambiar de canal - - -def ticks_bonitos(vmin, vmax, objetivo=6): - """Escalones redondos para el eje Y del canal que tenga el foco. - - Se descarta el tick del borde inferior porque se solaparía con la etiqueta - del eje de tiempos en la esquina.""" - rango = vmax - vmin - if rango <= 0: - return [] - mejor_paso, mejor_error = None, None - for exponente in range(-4, 6): - for multiplo in (1, 2, 5): - paso = multiplo * (10.0 ** exponente) - error = abs(rango / paso - objetivo) - if mejor_error is None or error < mejor_error: - mejor_paso, mejor_error = paso, error - primero = np.ceil(vmin / mejor_paso) * mejor_paso - ticks = np.arange(primero, vmax + mejor_paso * 0.5, mejor_paso) - return [t for t in ticks if t > vmin + rango * 0.001 and t <= vmax] - - -def aplicar_foco(clave): - """Reconfigura la gráfica grande para el canal indicado.""" - global canal_foco, adornos_foco - canal_foco = clave - cfg = CANALES[clave] - - for adorno in adornos_foco: - adorno.remove() - adornos_foco = [] - - ax_foco.set_ylim(cfg['vmin'], cfg['vmax']) - ax_foco.set_yticks(ticks_bonitos(cfg['vmin'], cfg['vmax'])) - ax_foco.set_ylabel(cfg['unidad'] or cfg['etiqueta'], fontsize=10, color=TXT_DIM) - - # Bandas de zona: se ve de un vistazo en qué régimen está el canal - inferior = cfg['vmin'] - for limite, color in cfg['zonas']: - superior = min(limite, cfg['vmax']) - if superior <= inferior: - continue - alpha = 0.05 if color in (AZUL, CIAN) else (0.14 if color == ROJO else 0.08) - adornos_foco.append(ax_foco.axhspan(inferior, superior, color=color, alpha=alpha, zorder=0)) - if color in (AMBAR, ROJO) and inferior > cfg['vmin']: - adornos_foco.append(ax_foco.axhline(inferior, color=color, lw=1, ls='--', - alpha=0.55, zorder=1)) - adornos_foco.append(ax_foco.text( - MAX_PUNTOS * 0.005, inferior + (cfg['vmax'] - cfg['vmin']) * 0.012, - f'{inferior:g} {cfg["unidad"]}'.strip(), fontsize=7.5, color=color, - ha='left', va='bottom', zorder=5, - bbox=dict(facecolor=PANEL, edgecolor='none', boxstyle='square,pad=0.25'))) - inferior = superior - - referencia = referencia_de(clave) - if referencia is not None: - adornos_foco.append(ax_foco.axhline(referencia, color=TXT_DIM, lw=1, ls=':', - alpha=0.7, zorder=2)) - - txt_foco_etiqueta.set_text(cfg['etiqueta']) - txt_foco_desc.set_text(cfg['descripcion']) - txt_foco_unidad.set_text(cfg['unidad']) - for tarjeta in TARJETAS: - tarjetas[tarjeta]['marco'].set_visible(tarjeta == clave) - - -# --- BANDA C: TARJETAS DE VIGILANCIA --- -gs_c = GridSpec(1, len(TARJETAS), left=0.035, right=0.975, top=0.282, bottom=0.052, - wspace=0.030) - -tarjetas = {} -for indice, clave in enumerate(TARJETAS): - cfg = CANALES[clave] - ax = fig.add_subplot(gs_c[0, indice]) - acento = preparar_panel(ax, TXT_DIM) - - # Marco que señala qué tarjeta está en la gráfica grande - marco = Rectangle((0.004, 0.01), 0.992, 0.98, transform=ax.transAxes, fill=False, - edgecolor=AZUL, lw=1.6, zorder=6) - marco.set_visible(False) - ax.add_patch(marco) - - ax.text(0.09, 0.86, cfg['etiqueta'], fontsize=10, fontweight='bold', color=TXT_DIM, va='center') - ax.text(0.955, 0.86, str(indice + 1), fontsize=8, color=TXT_DIM, ha='right', va='center') - txt_valor = ax.text(0.09, 0.60, '--', fontsize=26, fontweight='bold', color=TXT_DIM, va='center') - txt_unidad = ax.text(0.955, 0.55, cfg['unidad'], fontsize=10, color=TXT_DIM, - ha='right', va='center') - txt_extremos = ax.text(0.09, 0.36, '', fontsize=7, color=TXT_DIM, va='center') - - # Sparkline: cada canal lleva su propia historia, no solo el que tiene el foco. - # Se normaliza contra el rango fijo del canal, nunca autoescalado: si no, una - # señal plana con ruido de milésimas parecería una montaña rusa. - X0_SPARK, X1_SPARK, Y0_SPARK, Y1_SPARK = 0.09, 0.955, 0.10, 0.30 - linea_spark, = ax.plot([], [], color=TXT_DIM, lw=1.3, zorder=3, solid_capstyle='round') - linea_ref = Line2D([X0_SPARK, X1_SPARK], [0, 0], color=TXT_DIM, lw=0.8, ls=':', - alpha=0.6, zorder=2) - ax.add_line(linea_ref) - linea_ref.set_visible(cfg['referencia'] is not None) - - tarjetas[clave] = dict(ax=ax, acento=acento, marco=marco, valor=txt_valor, - unidad=txt_unidad, extremos=txt_extremos, spark=linea_spark, - ref=linea_ref, - caja=(X0_SPARK, X1_SPARK, Y0_SPARK, Y1_SPARK)) - -aplicar_foco(CANAL_FOCO_INICIAL) - - -def puntos_sparkline(clave, caja): - """Proyecta la historia del canal dentro del recuadro de su tarjeta.""" - x0, x1, y0, y1 = caja - cfg = CANALES[clave] - datos = np.array(historial[clave], dtype=float) - recorrido = cfg['vmax'] - cfg['vmin'] - if recorrido <= 0: - return [], [] - normalizado = (datos - cfg['vmin']) / recorrido - normalizado = np.clip(normalizado, 0.0, 1.0) - xs = np.linspace(x0, x1, len(datos)) - return xs, y0 + normalizado * (y1 - y0) - - -def y_sparkline(clave, valor, caja): - """Altura, dentro de la tarjeta, que corresponde a un valor del canal.""" - _, _, y0, y1 = caja - cfg = CANALES[clave] - recorrido = cfg['vmax'] - cfg['vmin'] - if recorrido <= 0: - return y0 - fraccion = min(max((valor - cfg['vmin']) / recorrido, 0.0), 1.0) - return y0 + fraccion * (y1 - y0) - - -# --- GRABACIÓN Y CAPTURA --- -def alternar_grabacion(): - """Arranca o detiene el volcado de la sesión a CSV. El fichero resultante es - el que se sube después a la plataforma web como registro de telemetría.""" - global grabando, fichero_csv, escritor_csv, inicio_grabacion, nombre_grabacion - global muestras_grabadas, texto_pie - - if grabando: - fichero_csv.close() - fichero_csv = None - escritor_csv = None - grabando = False - texto_pie = f'Guardado: sesiones/{nombre_grabacion} ({muestras_grabadas} muestras)' - return - - # Descartamos lo que estuviera encolado: son muestras anteriores a pulsar REC - try: - while True: - sock.recvfrom(1024) - except BlockingIOError: - pass - - os.makedirs(RUTA_SESIONES, exist_ok=True) - nombre_grabacion = f"sesion_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" - fichero_csv = open(os.path.join(RUTA_SESIONES, nombre_grabacion), 'w', newline='', encoding='utf-8') - escritor_csv = csv.writer(fichero_csv) - escritor_csv.writerow(['n_muestra', 'tiempo_s', 'hora'] + COLUMNAS_CSV) - inicio_grabacion = time.time() - muestras_grabadas = 0 - grabando = True - texto_pie = f'Grabando en sesiones/{nombre_grabacion}' - - -def mostrar_aviso(texto, segundos=4.0): - """Mensaje temporal en el pie, que después deja ver de nuevo el estado fijo.""" - global aviso_pie, aviso_hasta - aviso_pie = texto - aviso_hasta = time.time() + segundos - - -def capturar_pantalla(): - """Guarda la pantalla tal cual se ve. Sin diálogo de fichero: en boxes no hay - tiempo ni ratón cómodo para navegar por carpetas.""" - os.makedirs(RUTA_CAPTURAS, exist_ok=True) - nombre = f"captura_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" - fig.savefig(os.path.join(RUTA_CAPTURAS, nombre), dpi=110, facecolor=fig.get_facecolor()) - mostrar_aviso(f'Captura guardada: capturas/{nombre}') - - -def al_pulsar_tecla(event): - if event.key == 'r': - alternar_grabacion() - elif event.key == 's': - capturar_pantalla() - elif event.key and event.key.isdigit(): - indice = int(event.key) - 1 - if 0 <= indice < len(TARJETAS): - aplicar_foco(TARJETAS[indice]) - mostrar_aviso(f'Gráfica: {CANALES[TARJETAS[indice]]["descripcion"]}', 2.5) - - -fig.canvas.mpl_connect('key_press_event', al_pulsar_tecla) - - -def update(frame): - global ultimo_tiempo_dato, conectado, inicio_sesion - global paquetes_ok, paquetes_error, muestras_grabadas - - txt_reloj.set_text(datetime.now().strftime('%H:%M:%S')) - ahora = time.time() - hubo_dato = False - - # --- LECTURA DEL SOCKET (vaciamos todo lo pendiente) --- - try: - while True: - data, _ = sock.recvfrom(1024) - try: - lectura = parsear_mensaje(data.decode('utf-8')) - except (ValueError, UnicodeDecodeError): - paquetes_error += 1 - continue - - # Sello propio de cada paquete: en una misma pasada se vacían varios - # y compartir el instante del frame duplicaría marcas en el CSV - t_paquete = time.time() - ultimo_tiempo_dato = t_paquete - paquetes_ok += 1 - hubo_dato = True - sellos_tiempo.append(t_paquete) - if inicio_sesion is None: - inicio_sesion = t_paquete - - ultima_lectura.update(lectura) - for clave, valor in lectura.items(): - if clave in historial: - historial[clave].append(valor) - maximos[clave] = valor if clave not in maximos else max(maximos[clave], valor) - minimos[clave] = valor if clave not in minimos else min(minimos[clave], valor) - - if grabando: - muestras_grabadas += 1 - escritor_csv.writerow( - [muestras_grabadas, f'{t_paquete - inicio_grabacion:.3f}', - datetime.now().strftime('%H:%M:%S.%f')[:-3]] - + [valor_csv(col) for col in COLUMNAS_CSV]) - fichero_csv.flush() - except BlockingIOError: - pass - - conectado = (ahora - ultimo_tiempo_dato) <= TIMEOUT_SEG - - # --- ESTADO DEL ENLACE --- - if conectado: - hz = len([t for t in sellos_tiempo if ahora - t <= 1.0]) - txt_estado.set_text('EN LÍNEA') - txt_estado.set_color(VERDE) - led_estado.set_color(VERDE) - txt_hz.set_text(f'{hz} Hz') - txt_hz.set_color(TXT_DIM if hz >= FRECUENCIA_HZ * 0.7 else AMBAR) - else: - if not hubo_dato: - # Cortamos las líneas en vez de dibujar ceros falsos - for cola in historial.values(): - cola.append(np.nan) - ultima_lectura.clear() - txt_estado.set_text('SIN SEÑAL') - txt_estado.set_color(ROJO) - led_estado.set_color(ROJO) - txt_hz.set_text('-- Hz') - txt_hz.set_color(TXT_DIM) - - # --- BANDA A: RPM, VELOCIDAD, PEDALES --- - valor_rpm = ultima_lectura.get('rpm') if conectado else None - if valor_rpm is not None: - valor_rpm = max(0, min(MAX_RPM, valor_rpm)) - encendidos = int(round((valor_rpm / MAX_RPM) * N_SEGMENTOS)) - en_corte = valor_rpm >= RPM_CORTE - destello = en_corte and (frame // 4) % 2 == 0 - for i, seg in enumerate(segmentos): - if destello: - seg.set_facecolor(ROJO) - elif i < encendidos: - seg.set_facecolor(colores_segmento[i]) - else: - seg.set_facecolor(APAGADO) - txt_rpm.set_text(formatear('rpm', valor_rpm)) - txt_rpm.set_color(ROJO if en_corte else TXT) - else: - for seg in segmentos: - seg.set_facecolor(APAGADO) - txt_rpm.set_text('--') - txt_rpm.set_color(TXT_DIM) - - # Velocidad: número grande - valor_vel = ultima_lectura.get('velocidad') if conectado else None - txt_vel.set_text(formatear('velocidad', valor_vel)) - txt_vel.set_color(TXT if valor_vel is not None else TXT_DIM) - - # Traza de pedales: acelerador (0-100 %) y freno (presión -> % de FRENO_PRESION_MAX) - serie_tps = np.array(historial['tps'], dtype=float)[-PUNTOS_PEDALES:] - serie_freno = np.array(historial['freno_del'], dtype=float)[-PUNTOS_PEDALES:] - freno_pct = np.clip(serie_freno / FRENO_PRESION_MAX * 100.0, 0, 100) - xs_ped = np.arange(len(serie_tps), dtype=float) - line_tps.set_data(xs_ped, serie_tps) - line_freno.set_data(xs_ped, freno_pct) - fill_tps.set_xy(area_pedales(xs_ped, serie_tps)) - fill_freno.set_xy(area_pedales(xs_ped, freno_pct)) - - val_tps = ultima_lectura.get('tps') if conectado else None - val_freno = ultima_lectura.get('freno_del') if conectado else None - txt_ped_tps.set_text(f'ACEL {int(round(val_tps))}%' if val_tps is not None else 'ACEL --') - if val_freno is not None: - txt_ped_freno.set_text(f'FRENO {int(round(min(100.0, val_freno / FRENO_PRESION_MAX * 100)))}%') - else: - txt_ped_freno.set_text('FRENO --') - - # --- BANDA C: TARJETAS --- - alarmas = [] - for clave, widgets in tarjetas.items(): - valor = ultima_lectura.get(clave) if conectado else None - color = color_de(clave, valor) - widgets['valor'].set_text(formatear(clave, valor)) - widgets['valor'].set_color(color) - widgets['unidad'].set_color(TXT_DIM) - widgets['acento'].set_facecolor(color) - widgets['spark'].set_color(color if valor is not None else TXT_DIM) - - xs, ys = puntos_sparkline(clave, widgets['caja']) - widgets['spark'].set_data(xs, ys) - - referencia = referencia_de(clave) - if referencia is not None: - y_ref = y_sparkline(clave, referencia, widgets['caja']) - widgets['ref'].set_ydata([y_ref, y_ref]) - widgets['ref'].set_visible(True) - else: - widgets['ref'].set_visible(False) - - if clave in maximos: - widgets['extremos'].set_text( - f'máx {formatear(clave, maximos[clave])} mín {formatear(clave, minimos[clave])}') - else: - widgets['extremos'].set_text('') - - if color == ROJO and valor is not None: - alarmas.append(CANALES[clave]['etiqueta']) - - # --- BANDA B: CANAL CON FOCO --- - valor_foco = ultima_lectura.get(canal_foco) if conectado else None - color_foco = color_de(canal_foco, valor_foco) - txt_foco_valor.set_text(formatear(canal_foco, valor_foco)) - txt_foco_valor.set_color(color_foco) - acento_foco.set_facecolor(color_foco) - line_foco.set_color(color_foco if valor_foco is not None else TXT_DIM) - line_foco.set_data(range(MAX_PUNTOS), historial[canal_foco]) - - if valor_foco is None: - txt_foco_estado.set_text('SIN SEÑAL' if not conectado else 'SIN DATO') - txt_foco_estado.set_color(TXT_DIM) - punto_foco.set_data([], []) - else: - if color_foco == ROJO: - estado = 'CRÍTICO' - elif color_foco == AMBAR: - estado = 'PRECAUCIÓN' - elif color_foco == CIAN: - estado = 'EN CALENTAMIENTO' - else: - estado = 'NORMAL' - txt_foco_estado.set_text(estado) - txt_foco_estado.set_color(color_foco) - punto_foco.set_color(color_foco) - punto_foco.set_data([MAX_PUNTOS - 1], [valor_foco]) - - # --- ALARMA --- - if alarmas: - txt_alarma.set_text(' ALARMA: ' + ' · '.join(alarmas) + ' ') - txt_alarma.set_visible(True) - encendida = (frame // 5) % 2 == 0 - txt_alarma.get_bbox_patch().set_facecolor(ROJO if encendida else '#6E1414') - else: - txt_alarma.set_visible(False) - - # --- INDICADOR DE GRABACIÓN Y PIE --- - if grabando: - parpadeo = (frame // 6) % 2 == 0 - txt_rec.set_text(f'REC {formato_tiempo(ahora - inicio_grabacion)}') - txt_rec.set_color(ROJO) - led_rec.set_color(ROJO if parpadeo else '#4A1414') - else: - txt_rec.set_text('SIN GRABAR') - txt_rec.set_color(TXT_DIM) - led_rec.set_color(APAGADO) - - sesion = formato_tiempo(None if inicio_sesion is None else ahora - inicio_sesion) - errores = f' · {paquetes_error} con error' if paquetes_error else '' - txt_sesion.set_text(f'SESIÓN {sesion} · {paquetes_ok:,}'.replace(',', '.') - + f' paquetes{errores}') - txt_fichero.set_text(aviso_pie if ahora < aviso_hasta else texto_pie) - - return () - - -ani = animation.FuncAnimation(fig, update, interval=60, blit=False, cache_frame_data=False) - -# Arrancamos maximizado y sin barra de herramientas (pantalla de boxes) -try: - fig.canvas.manager.window.state('zoomed') -except Exception: - try: - fig.canvas.manager.full_screen_toggle() - except Exception: - pass - -plt.show() - -if fichero_csv is not None: - fichero_csv.close() diff --git a/Firmware/G26-Telemetria/G26-Telemetria.ino b/Firmware/G26-Telemetria/G26-Telemetria.ino deleted file mode 100644 index 2dabd64..0000000 --- a/Firmware/G26-Telemetria/G26-Telemetria.ino +++ /dev/null @@ -1,136 +0,0 @@ -#include "include/data_processor.hpp" -#include "include/can.hpp" -#include "include/common_libraries.hpp" - -DataProcessor dataProcessor; -CAN canController; - -// SD -SPIClass spiSD(HSPI); -SdFat sd; -SdFile logFile; - -// UDP -WiFiUDP udp; - -// --- Tarea UDP (Nucleo 0) --- -void TaskUdpSender(void *pvParameters) { - Serial.println("Iniciando tarea de envio UDP..."); - - while (true) { - if (WiFi.status() == WL_CONNECTED) { - int tempActual = dataProcessor.current_ect_value; - int rpmActual = dataProcessor.current_rpm_value; - float battActual = dataProcessor.current_vbatt_value; - float tpsActual = dataProcessor.current_tps_value; - float frenoDelActual = dataProcessor.current_freno_del_value; - float pcombActual = dataProcessor.current_pcomb_value; - float taceiteActual = dataProcessor.current_taceite_value; - float paceiteActual = dataProcessor.current_paceite_value; - float mapActual = dataProcessor.current_map_value; - float lambdaActual = dataProcessor.current_lambda_value; - float lambdaObjActual = dataProcessor.current_lambda_obj_value; - - char mensaje[256]; - snprintf(mensaje, sizeof(mensaje), - "ect=%d;rpm=%d;vbatt=%.2f;tps=%.1f;freno_del=%.1f;" - "pcomb=%.2f;taceite=%.1f;paceite=%.2f;map=%.1f;lambda=%.3f;lambda_obj=%.3f", - tempActual, rpmActual, battActual, tpsActual, frenoDelActual, - pcombActual, taceiteActual, paceiteActual, mapActual, lambdaActual, lambdaObjActual); - - udp.beginPacket(IPAddress(255, 255, 255, 255), UDP_PORT); - udp.print(mensaje); - udp.endPacket(); - } else { - Serial.println("[WIFI] Desconectado..."); - WiFi.disconnect(); - WiFi.reconnect(); - } - - vTaskDelay(50 / portTICK_PERIOD_MS); - } -} - -void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("\n--- G26 TELEMETRY: INICIO DE SISTEMA ---"); - - // 1. INICIALIZACION SD - spiSD.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS); - - if (!sd.begin(SdSpiConfig(SD_CS, DEDICATED_SPI, SD_SCK_MHZ(1), &spiSD))) { - Serial.println("[FALLO] SD no detectada. El sistema continuara sin Datalogging."); - } else { - char filename[24]; - int session = 1; - bool opened = false; - - while (!opened && session < 100000) { - snprintf(filename, sizeof(filename), "G26-%d.csv", session); - if (logFile.open(filename, O_RDWR | O_CREAT | O_EXCL)) { - opened = true; - Serial.printf("[OK] Nueva sesion: %s\n", filename); - } else { - session++; - } - } - - if (opened) { - logFile.println("Time,ECT,RPM,TPS,VBATT,FRENO_DEL,PCOMB,TACEITE,PACEITE,MAP,LAMBDA,LAMBDA_OBJ"); - logFile.sync(); - } else { - Serial.println("[ERROR] No se pudo abrir archivo de sesion"); - } - } - - dataProcessor.setLogSystem(&sd, &logFile); - - // 2. INICIAR CAN - canController.set_data_proccessor(&dataProcessor); - canController.start(); - canController.start_listening_task(); - - // 3. INICIAR WIFI - Serial.println("--- CONECTANDO WIFI ---"); - - IPAddress local_IP(192, 168, 0, 50); - IPAddress gateway(192, 168, 0, 254); - IPAddress subnet(255, 255, 255, 0); - - if (!WiFi.config(local_IP, gateway, subnet)) { - Serial.println("[ERR] Fallo al configurar IP estatica"); - } - - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - - int intentos = 0; - while (WiFi.status() != WL_CONNECTED && intentos < 20) { - delay(500); - Serial.print("."); - intentos++; - } - - if (WiFi.status() == WL_CONNECTED) { - Serial.println("\n[OK] WiFi Conectado."); - } else { - Serial.println("\n[ERR] No se pudo conectar WiFi (Continuando offline)."); - } - - // 4. TAREA UDP - xTaskCreatePinnedToCore( - TaskUdpSender, - "UdpSender", - 4096, - NULL, - 1, - NULL, - 0 - ); - - Serial.println("[OK] Sistema ONLINE (CAN + SD + WiFi)"); -} - -void loop() { - vTaskDelay(5 / portTICK_PERIOD_MS); -} diff --git a/Firmware/G26-Telemetria/include/common_libraries.hpp b/Firmware/G26-Telemetria/include/common_libraries.hpp deleted file mode 100644 index 7cfab84..0000000 --- a/Firmware/G26-Telemetria/include/common_libraries.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef COMMON_LIBRARIES_HPP -#define COMMON_LIBRARIES_HPP - -#include -#include "time.h" -#include -#include - -// --- WiFi y UDP --- -#include -#include - -// --- SD --- -#include -#include "SdFat.h" - -// CONFIGURACION WIFI -#define WIFI_SSID "FGades" -#define WIFI_PASSWORD "GadesCPE" - -// CONFIGURACION UDP -#define UDP_PORT 4210 - -// CONFIGURACION SD (HSPI) -#define SD_CS 25 -#define SD_MOSI 26 -#define SD_SCK 27 -#define SD_MISO 14 - -#endif diff --git a/Firmware/G26-Telemetria/include/data_processor.hpp b/Firmware/G26-Telemetria/include/data_processor.hpp deleted file mode 100644 index 64c1fce..0000000 --- a/Firmware/G26-Telemetria/include/data_processor.hpp +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef DATAPROCESSOR_HPP -#define DATAPROCESSOR_HPP - -#include "common_libraries.hpp" -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" - -class DataProcessor { -public: - DataProcessor() = default; - - // Variables CAN (actualizadas por los frames) - volatile int current_ect_value = 0; - volatile int current_rpm_value = 0; - volatile float current_vbatt_value = 0.0; - - volatile float current_tps_value = 0.0; - volatile float current_freno_del_value = 0.0; - volatile float current_pcomb_value = 0.0; - volatile float current_taceite_value = 0.0; - volatile float current_paceite_value = 0.0; - volatile float current_map_value = 0.0; - volatile float current_lambda_value = 0.0; - volatile float current_lambda_obj_value = 0.0; - - // Pendientes de instalar - volatile float current_freno_tra_value = 0.0; - volatile float current_velocidad_value = 0.0; - - // Configuracion SD - void setLogSystem(SdFat* sd_inst, SdFile* file_inst) { - _sd = sd_inst; - _logFile = file_inst; - } - - // Metodos CAN - void send_serial_frame_0(int rpmh, int rpml, int tpsh, int tpsl, int vbatth, int vbattl, int ect); - void send_serial_frame_1(int lmbh, int lmbl, int lmbth, int lmbtl, int fuelh, int fuell, int gear); - void send_serial_frame_2(int shut, int fan, int lmbch, int lmbcl, int brakeh, int brakel, int aux1); - void send_serial_frame_3(int oilth, int oiltl, int oilph, int oilpl, int maph, int mapl, int dig1); - void send_serial_frame_4(int dig3, int dig4, int dig5, int dig6, int dig7, int dig8, int dig9); - - void send_serial(byte type, unsigned int value); - - char* process(std::vector data); - -private: - SdFat* _sd = nullptr; - SdFile* _logFile = nullptr; - - uint32_t _last_sync_time = 0; - const uint32_t _sync_interval_ms = 1000; - - void flushToSD(); -}; - -#endif diff --git a/Firmware/G26-Telemetria/src/data_processor.cpp b/Firmware/G26-Telemetria/src/data_processor.cpp deleted file mode 100644 index a271eaf..0000000 --- a/Firmware/G26-Telemetria/src/data_processor.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "../include/data_processor.hpp" - -char* DataProcessor::process(std::vector data) { - return nullptr; -} - -void DataProcessor::send_serial(byte type, unsigned int value) { - byte dato[8] = { 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x00, 0x00, 0x00 }; - dato[4] = type; - dato[6] = (value >> 8) & 0xFF; - dato[7] = value & 0xFF; - Serial.write(dato, 8); -} - -// RPM + TPS + vBatt + ECT -void DataProcessor::send_serial_frame_0(int rpmh, int rpml, int tpsh, int tpsl, int vbatth, int vbattl, int ect) { - int rpm = (rpmh * 256) + rpml; - double vbatt = ((vbatth * 256) + vbattl) / 100.0; - int tps = (tpsh * 256) + tpsl; - - this->current_ect_value = ect; - this->current_rpm_value = rpm; - this->current_vbatt_value = vbatt; - this->current_tps_value = tps; - - flushToSD(); -} - -void DataProcessor::send_serial_frame_1(int lmbh, int lmbl, int lmbth, int lmbtl, int fuelh, int fuell, int gear) { - float lambda = ((lmbh * 256) + lmbl) / 100.0; - float lambdaTarget = ((lmbth * 256) + lmbtl) / 100.0; - float presionComb = ((fuelh * 256) + fuell) / 100.0; - - this->current_lambda_value = lambda; - this->current_lambda_obj_value = lambdaTarget; - this->current_pcomb_value = presionComb; - flushToSD(); - -} - -void DataProcessor::send_serial_frame_2(int shut, int fan, int lmbch, int lmbcl, int brakeh, int brakel, int aux1) { - float freno = (brakeh * 256) + brakel; - this->current_freno_del_value = freno; - flushToSD(); - -} - -void DataProcessor::send_serial_frame_3(int oilth, int oiltl, int oilph, int oilpl, int maph, int mapl, int dig1) { - float tempOil = ((oilth * 256) + oiltl) / 100.0; - float presionOil = ((oilph * 256) + oilpl) / 100.0; - float map = ((maph * 256) + mapl) / 100.0; - - this->current_taceite_value = tempOil; - this->current_paceite_value = presionOil; - this->current_map_value = map; - flushToSD(); - -} - -void DataProcessor::send_serial_frame_4(int dig3, int dig4, int dig5, int dig6, int dig7, int dig8, int dig9) { -} - -// --- ESCRITURA SD --- - -void DataProcessor::flushToSD() { - if (_sd && _logFile && _logFile->isOpen()) { - char buffer[256]; - int len = snprintf(buffer, sizeof(buffer), - "%lu,%d,%d,%.1f,%.2f,%.1f,%.2f,%.1f,%.2f,%.1f,%.3f,%.3f\n", - millis(), - current_ect_value, - current_rpm_value, - current_tps_value, - current_vbatt_value, - current_freno_del_value, - current_pcomb_value, - current_taceite_value, - current_paceite_value, - current_map_value, - current_lambda_value, - current_lambda_obj_value); - - _logFile->write(buffer, len); - - if (millis() - _last_sync_time > _sync_interval_ms) { - _logFile->sync(); - _last_sync_time = millis(); - } - } -} diff --git a/G26-Telemetria.ino b/G26-Telemetria.ino new file mode 100644 index 0000000..271da27 --- /dev/null +++ b/G26-Telemetria.ino @@ -0,0 +1,48 @@ +#include "include/data_processor.hpp" +#include "include/can.hpp" +#include "include/g24_wheel_buttons.hpp" +#include "include/led_strip.hpp" +#include "include/crowpanel_controller.hpp" + +#include +#include + +DataProcessor dataProcessor; +CAN canController; +G24WheelButtons wheelButtons; +LedStrip ledStrip; +CrowPanelController crowPanelController; + +// Screen rotation variables +unsigned long lastScreenChange = 0; +int currentScreen = 1; +int screenCycle = 0; // 0 = screen1, 1 = screen2, 2 = screen3, 3 = screen4 + +void setup() { + Serial.begin(115200); + while (!Serial) { delay(10); } + Serial.println("Starting setup..."); + canController.set_data_proccessor(&dataProcessor); + dataProcessor.set_led_strip(&ledStrip); + dataProcessor.set_crow_panel_controller(&crowPanelController); + // wheelButtons.set_led_strip(&ledStrip); + // wheelButtons.set_can_controller(&canController); + // wheelButtons.set_data_processor(&dataProcessor); + // ledStrip.set_mutex(canController.get_mutex()); + + canController.start(); + canController.start_listening_task(); + + + // wheelButtons.begin(); + + // xTaskCreate(wheelButtons.updateTask, "updateTask", 4096, &wheelButtons, 1, NULL); + + // Initialize with screen 1 + lastScreenChange = millis(); +} + +void loop(){ + lv_timer_handler(); + vTaskDelay(5); +} \ No newline at end of file diff --git a/Plataform_Web/documentos/__init__.py b/Plataform_Web/documentos/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/documentos/admin.py b/Plataform_Web/documentos/admin.py deleted file mode 100644 index d14f08f..0000000 --- a/Plataform_Web/documentos/admin.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.contrib import admin -from .models import Documento, Factura - -@admin.register(Documento) -class DocumentoAdmin(admin.ModelAdmin): - # Columnas principales - list_display = ('nombre', 'categoria', 'tipo', 'subido_por', 'fecha_subida') - - # Filtros por área técnica, tipo de documento y temporada del documento - list_filter = ('categoria', 'tipo', 'temporada') - - # Buscador de documentos por su nombre o descripción - search_fields = ('nombre', 'descripcion') - - # Para asignar a que temporada pertenece el documento - filter_horizontal = ('temporada',) - - # Automatización: para rellenar el creador del nuevo documento de manera automática - def save_model(self, request, obj, form, change): - if not obj.subido_por: - obj.subido_por = request.user - super().save_model(request, obj, form, change) - - -@admin.register(Factura) -class FacturaAdmin(admin.ModelAdmin): - # Herncia de Documento - list_display = ('nombre', 'empresa', 'importe', 'categoria', 'subido_por', 'fecha_subida') - - list_filter = ('categoria', 'temporada') - search_fields = ('nombre', 'empresa', 'descripcion') - - filter_horizontal = ('temporada',) - - # Mantenemos la misma automatización - def save_model(self, request, obj, form, change): - if not obj.subido_por: - obj.subido_por = request.user - super().save_model(request, obj, form, change) \ No newline at end of file diff --git a/Plataform_Web/documentos/apps.py b/Plataform_Web/documentos/apps.py deleted file mode 100644 index b42b59d..0000000 --- a/Plataform_Web/documentos/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class DocumentosConfig(AppConfig): - name = 'documentos' diff --git a/Plataform_Web/documentos/migrations/0001_initial.py b/Plataform_Web/documentos/migrations/0001_initial.py deleted file mode 100644 index 3598421..0000000 --- a/Plataform_Web/documentos/migrations/0001_initial.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 6.0.2 on 2026-02-17 11:36 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('temporadas', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Documento', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('titulo', models.CharField(max_length=100, verbose_name='Título del documento')), - ('archivo', models.FileField(upload_to='ingenieria_docs/')), - ('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('suspension', 'Suspensión'), ('motor', 'Motor/Powertrain'), ('electronica', 'Electrónica'), ('general', 'General / Normativa')], max_length=20)), - ('tipo', models.CharField(choices=[('diseno', 'Diseño / CAD'), ('simulacion', 'Simulación'), ('informe', 'Informe Técnico'), ('factura', 'Factura / Presupuesto'), ('otro', 'Otro')], default='informe', max_length=20)), - ('descripcion', models.TextField(blank=True, null=True, verbose_name='Descripción o notas')), - ('fecha_subida', models.DateTimeField(auto_now_add=True)), - ('subido_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documentos_subidos', to=settings.AUTH_USER_MODEL)), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')), - ], - options={ - 'verbose_name': 'Documento Técnico', - 'verbose_name_plural': 'Documentos de Ingeniería', - 'ordering': ['-fecha_subida'], - }, - ), - ] diff --git a/Plataform_Web/documentos/migrations/0002_factura_alter_documento_options_and_more.py b/Plataform_Web/documentos/migrations/0002_factura_alter_documento_options_and_more.py deleted file mode 100644 index d29ff00..0000000 --- a/Plataform_Web/documentos/migrations/0002_factura_alter_documento_options_and_more.py +++ /dev/null @@ -1,66 +0,0 @@ -# Generated by Django 6.0.2 on 2026-03-06 19:29 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documentos', '0001_initial'), - ('temporadas', '0002_temporada_miembros'), - ] - - operations = [ - migrations.CreateModel( - name='Factura', - fields=[ - ('documento_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='documentos.documento')), - ('empresa', models.CharField(max_length=100, verbose_name='Nombre de la empresa')), - ('importe', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Importe (€)')), - ], - options={ - 'verbose_name': 'Factura', - 'verbose_name_plural': 'Facturas', - }, - bases=('documentos.documento',), - ), - migrations.AlterModelOptions( - name='documento', - options={'ordering': ['-fecha_subida'], 'verbose_name': 'Documento', 'verbose_name_plural': 'Documentos'}, - ), - migrations.RemoveField( - model_name='documento', - name='titulo', - ), - migrations.AddField( - model_name='documento', - name='nombre', - field=models.CharField(default='Documento antiguo', max_length=100, verbose_name='Nombre del documento'), - preserve_default=False, - ), - migrations.AlterField( - model_name='documento', - name='categoria', - field=models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('e_powertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('normativa', 'General / Normativa')], max_length=20), - ), - migrations.AlterField( - model_name='documento', - name='descripcion', - field=models.TextField(blank=True, null=True, verbose_name='Descripción del documento'), - ), - migrations.RemoveField( - model_name='documento', - name='temporada', - ), - migrations.AlterField( - model_name='documento', - name='tipo', - field=models.CharField(choices=[('fabricacion', 'Fabricación'), ('diseno', 'Diseño / CAD'), ('concepto', 'Concepto'), ('simulacion', 'Simulación'), ('dossier_patrocinado', 'Dossier Patrocinio'), ('informe', 'Informe'), ('tutorial', 'Tutorial'), ('otro', 'Otro')], default='informe', max_length=20), - ), - migrations.AddField( - model_name='documento', - name='temporada', - field=models.ManyToManyField(related_name='documentos_asociados', to='temporadas.temporada'), - ), - ] diff --git a/Plataform_Web/documentos/migrations/0003_factura_estado.py b/Plataform_Web/documentos/migrations/0003_factura_estado.py deleted file mode 100644 index 0d89bea..0000000 --- a/Plataform_Web/documentos/migrations/0003_factura_estado.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 6.0.2 on 2026-06-30 16:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('documentos', '0002_factura_alter_documento_options_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='factura', - name='estado', - field=models.CharField(choices=[('pendiente', 'Pendiente'), ('aceptada', 'Aceptada'), ('rechazada', 'Rechazada')], default='pendiente', max_length=20), - ), - ] diff --git a/Plataform_Web/documentos/migrations/__init__.py b/Plataform_Web/documentos/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/documentos/models.py b/Plataform_Web/documentos/models.py deleted file mode 100644 index 2703e84..0000000 --- a/Plataform_Web/documentos/models.py +++ /dev/null @@ -1,83 +0,0 @@ -from django.db import models -from users.models import CustomUser -from temporadas.models import Temporada -import os - -class Documento(models.Model): - # Opciones de categorías (puedes añadir más) - CATEGORIAS = ( - ('aerodinamica', 'Aerodinámica'), - ('chasis', 'Chasis'), - ('business', 'Business & Operations'), - ('e_powertrain', 'E-Powertrain'), - ('electronica', 'Electrónica'), - ('sdf', 'SDF'), - ('motor_transmision', 'Motor & Transmisión'), - ('software', 'Software'), - ('normativa', 'General / Normativa'), - ) - - TIPO_DOC = ( - ('fabricacion', 'Fabricación'), - ('diseno', 'Diseño / CAD'), - ('concepto', 'Concepto'), - ('simulacion', 'Simulación'), - ('dossier_patrocinado', 'Dossier Patrocinio'), - ('informe', 'Informe'), - ('tutorial', 'Tutorial'), - ('otro', 'Otro'), - ) - - # CAMPOS - nombre = models.CharField(max_length=100, verbose_name="Nombre del documento") - - # Aquí definimos dónde se guardan los archivos. - # upload_to='ingenieria/' creará esa carpeta automáticamente. - archivo = models.FileField(upload_to='ingenieria_docs/') - - categoria = models.CharField(max_length=20, choices=CATEGORIAS) - tipo = models.CharField(max_length=20, choices=TIPO_DOC, default='informe') - descripcion = models.TextField(blank=True, null=True, verbose_name="Descripción del documento") - fecha_subida = models.DateTimeField(auto_now_add=True) - - # RELACIONES - # Relacion Documento - Temporadas (Relación 1..* a 1..* ) - temporada = models.ManyToManyField(Temporada, related_name="documentos_asociados") - - # 2. Relación 1 a N con el Usuario (Miembros) - subido_por = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True, related_name="documentos_subidos") - - class Meta: - verbose_name = "Documento" - verbose_name_plural = "Documentos" - ordering = ['-fecha_subida'] - - def __str__(self): - return f"{self.nombre} ({self.get_categoria_display()})" - - def delete(self, *args, **kwargs): - # Esto borra el archivo físico del disco duro cuando borras la entrada en la base de datos - if self.archivo: - if os.path.isfile(self.archivo.path): - os.remove(self.archivo.path) - super().delete(*args, **kwargs) - -ESTADO_FACTURA = [ - ('pendiente', 'Pendiente'), - ('aceptada', 'Aceptada'), - ('rechazada', 'Rechazada'), -] - -# Herencia de la clase Factura: Factura hereda de Documento -class Factura(Documento): - # Al heredar de Documento, ya tiene nombre, archivo, categoria, etc. - empresa = models.CharField(max_length=100, verbose_name="Nombre de la empresa") - importe = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Importe (€)") - estado = models.CharField(max_length=20, choices=ESTADO_FACTURA, default='pendiente') - - class Meta: - verbose_name = "Factura" - verbose_name_plural = "Facturas" - - def __str__(self): - return f"Factura {self.empresa} - {self.importe}€" \ No newline at end of file diff --git a/Plataform_Web/documentos/tests.py b/Plataform_Web/documentos/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/Plataform_Web/documentos/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Plataform_Web/documentos/views.py b/Plataform_Web/documentos/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/Plataform_Web/documentos/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/Plataform_Web/gades_manager/__init__.py b/Plataform_Web/gades_manager/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/gades_manager/asgi.py b/Plataform_Web/gades_manager/asgi.py deleted file mode 100644 index 153ca09..0000000 --- a/Plataform_Web/gades_manager/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for gades_manager project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings') - -application = get_asgi_application() diff --git a/Plataform_Web/gades_manager/settings.py b/Plataform_Web/gades_manager/settings.py deleted file mode 100644 index a0536f4..0000000 --- a/Plataform_Web/gades_manager/settings.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -Django settings for gades_manager project. - -Generated by 'django-admin startproject' using Django 6.0.2. - -For more information on this file, see -https://docs.djangoproject.com/en/6.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/6.0/ref/settings/ -""" - -from pathlib import Path -import os - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-mwlyo^0ax9u9ts5(hw(e&vlgmr$nbla-3az0d%s(lymmp5^)$-' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'users', - 'temporadas', - 'documentos', - 'gestion', - 'pruebas', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'gades_manager.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'gades_manager.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/6.0/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'formula_gades_bd', # El nombre exacto que pusiste en Workbench - 'USER': 'root', - 'PASSWORD': 'admin1234', # ¡La que apuntaste en el papel! - 'HOST': 'localhost', # Significa "este ordenador" - 'PORT': '3306', - } -} - - -# Password validation -# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/6.0/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/6.0/howto/static-files/ - -STATIC_URL = '/static/' - -STATICFILES_DIRS = [ - BASE_DIR / "static", -] - -AUTH_USER_MODEL = 'users.CustomUser' - -MEDIA_URL = '/media/' # La URL pública (ej: tudominio.com/media/archivo.pdf) -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # La carpeta física en tu ordenador - -LOGIN_URL = 'login' -LOGIN_REDIRECT_URL = 'inicio' -LOGOUT_REDIRECT_URL = 'login' diff --git a/Plataform_Web/gades_manager/urls.py b/Plataform_Web/gades_manager/urls.py deleted file mode 100644 index 6d53cf2..0000000 --- a/Plataform_Web/gades_manager/urls.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -URL configuration for gades_manager project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/6.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" - -from django.contrib import admin -from django.urls import path -from django.contrib.auth import views as auth_views -from gestion import views -from users import views as users_views -from temporadas import views as temporadas_views -from pruebas import views as pruebas_views - -urlpatterns = [ - path('admin/', admin.site.urls), - path('', views.inicio, name='inicio'), - path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'), - path('logout/', auth_views.LogoutView.as_view(next_page='login'), name='logout'), - path('mi-perfil/', users_views.mi_perfil, name='mi_perfil'), - path('miembros/', users_views.listado_miembros, name='listado_miembros'), - path('gestion/usuarios/', users_views.gestion_usuarios, name='gestion_usuarios'), - path('gestion/usuarios//editar/', users_views.editar_usuario, name='editar_usuario'), - path('gestion/usuarios//eliminar/', users_views.eliminar_usuario, name='eliminar_usuario'), - path('gestion/temporadas/', temporadas_views.gestion_temporadas, name='gestion_temporadas'), - path('gestion/temporadas/crear/', temporadas_views.crear_temporada, name='crear_temporada'), - path('gestion/temporadas//editar/', temporadas_views.editar_temporada, name='editar_temporada'), - path('gestion/temporadas//eliminar/', temporadas_views.eliminar_temporada, name='eliminar_temporada'), - path('gestion/contabilidad/', views.contabilidad, name='contabilidad'), - path('gestion/contabilidad/gasto/anadir/', views.anadir_gasto, name='anadir_gasto'), - path('gestion/contabilidad/ingreso/anadir/', views.anadir_ingreso, name='anadir_ingreso'), - path('gestion/contabilidad/factura//aceptar/', views.aceptar_factura, name='aceptar_factura'), - path('gestion/contabilidad/factura//rechazar/', views.rechazar_factura, name='rechazar_factura'), - path('patrocinios/', views.patrocinios, name='patrocinios'), - path('patrocinios/proponer/', views.proponer_patrocinio, name='proponer_patrocinio'), - path('patrocinios//editar/', views.editar_patrocinio, name='editar_patrocinio'), - path('patrocinios//estado/', views.cambiar_estado_patrocinio, name='cambiar_estado_patrocinio'), - path('pruebas/', pruebas_views.listado_pruebas, name='listado_pruebas'), - path('pruebas/nueva/', pruebas_views.crear_prueba, name='crear_prueba'), - path('pruebas//', pruebas_views.detalle_prueba, name='detalle_prueba'), - path('pruebas//editar/', pruebas_views.editar_prueba, name='editar_prueba'), - path('pruebas//eliminar/', pruebas_views.eliminar_prueba, name='eliminar_prueba'), - path('pruebas//csv/subir/', pruebas_views.subir_csv, name='subir_csv'), -] \ No newline at end of file diff --git a/Plataform_Web/gades_manager/wsgi.py b/Plataform_Web/gades_manager/wsgi.py deleted file mode 100644 index ef3583b..0000000 --- a/Plataform_Web/gades_manager/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for gades_manager project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings') - -application = get_wsgi_application() diff --git a/Plataform_Web/gestion/__init__.py b/Plataform_Web/gestion/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/gestion/admin.py b/Plataform_Web/gestion/admin.py deleted file mode 100644 index 0d47392..0000000 --- a/Plataform_Web/gestion/admin.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.contrib import admin -from .models import Patrocinio, Pieza, Gasto, Ingreso - -# INLINES -class PiezaInline(admin.TabularInline): - model = Pieza - extra = 1 - -# PANEL PARA ADMINISTRACIÓN -@admin.register(Patrocinio) -class PatrocinioAdmin(admin.ModelAdmin): - list_display = ('empresa', 'estado', 'tipo_patrocinio', 'importe_economico', 'temporada', 'contacto_equipo') - - # Para editar el estado y pasar de En contacto a Aceptado o Denagado desde la lista, sin tener que hacerlo manual - list_editable = ('estado',) - - list_filter = ('estado', 'tipo_patrocinio', 'temporada') - search_fields = ('empresa', 'persona_contacto', 'email_contacto') - - # Metemos las piezas en el patrocinio, por si hiciera falta - inlines = [PiezaInline] - -@admin.register(Pieza) -class PiezaAdmin(admin.ModelAdmin): - list_display = ('nombre', 'cantidad', 'patrocinio') - search_fields = ('nombre', 'patrocinio__empresa') - -# CONTABILIDAD (Gastos e Ingresos) -@admin.register(Gasto) -class GastoAdmin(admin.ModelAdmin): - # Heredamos los campos de Contabilidad - list_display = ('concepto', 'importe', 'categoria', 'fecha', 'temporada') - list_filter = ('categoria', 'temporada') - search_fields = ('concepto', 'observaciones') - - # Barra de navegación por fecha de gasto - date_hierarchy = 'fecha' - -@admin.register(Ingreso) -class IngresoAdmin(admin.ModelAdmin): - list_display = ('concepto', 'importe', 'categoria', 'fecha', 'temporada') - list_filter = ('categoria', 'temporada') - search_fields = ('concepto', 'observaciones') - - # Barra de navegación por fecha de ingreso - date_hierarchy = 'fecha' \ No newline at end of file diff --git a/Plataform_Web/gestion/apps.py b/Plataform_Web/gestion/apps.py deleted file mode 100644 index 48586ea..0000000 --- a/Plataform_Web/gestion/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class GestionConfig(AppConfig): - name = 'gestion' diff --git a/Plataform_Web/gestion/forms.py b/Plataform_Web/gestion/forms.py deleted file mode 100644 index aecfd67..0000000 --- a/Plataform_Web/gestion/forms.py +++ /dev/null @@ -1,46 +0,0 @@ -from django import forms -from .models import Gasto, Ingreso, Patrocinio - - -class GastoForm(forms.ModelForm): - class Meta: - model = Gasto - fields = ['concepto', 'importe', 'categoria', 'observaciones'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for field in self.fields.values(): - field.widget.attrs['class'] = 'form-control' - - -class IngresoForm(forms.ModelForm): - class Meta: - model = Ingreso - fields = ['concepto', 'importe', 'categoria', 'observaciones'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for field in self.fields.values(): - field.widget.attrs['class'] = 'form-control' - - -class PatrocinioForm(forms.ModelForm): - class Meta: - model = Patrocinio - fields = ['empresa', 'email_contacto', 'persona_contacto', 'tipo_patrocinio'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for field in self.fields.values(): - field.widget.attrs['class'] = 'form-control' - - -class PatrocinioEditForm(forms.ModelForm): - class Meta: - model = Patrocinio - fields = ['empresa', 'email_contacto', 'persona_contacto', 'tipo_patrocinio', 'estado', 'importe_economico'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for field in self.fields.values(): - field.widget.attrs['class'] = 'form-control' diff --git a/Plataform_Web/gestion/migrations/0001_initial.py b/Plataform_Web/gestion/migrations/0001_initial.py deleted file mode 100644 index 7c34fb8..0000000 --- a/Plataform_Web/gestion/migrations/0001_initial.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 6.0.2 on 2026-02-19 10:14 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('temporadas', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Factura', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=100, verbose_name='Concepto de la factura')), - ('empresa', models.CharField(max_length=100, verbose_name='Empresa / Proveedor')), - ('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], max_length=30)), - ('descripcion', models.TextField(blank=True, null=True, verbose_name='Breve descripción')), - ('archivo', models.FileField(upload_to='facturas/', verbose_name='Foto o PDF de la factura')), - ('subido_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Usuario que la sube')), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')), - ], - ), - migrations.CreateModel( - name='Patrocinador', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('empresa', models.CharField(max_length=100, verbose_name='Nombre de la Empresa')), - ('email_contacto', models.EmailField(max_length=254, verbose_name='Correo de contacto')), - ('tipo_empresa', models.CharField(max_length=30, verbose_name='Tipo de la Empresa')), - ('tipo_patrocinio', models.CharField(choices=[('economico', 'Económico'), ('piezas/materiales', 'Piezas/Materiales'), ('mixto', 'Mixto (Dinero y Piezas)')], max_length=30)), - ('detalle_piezas', models.TextField(blank=True, help_text='Detallar qué se ofrece si es en especies', null=True)), - ('estado', models.CharField(choices=[('en contacto', 'En contacto'), ('aceptado', 'Aceptado / Activo'), ('denegado', 'Denegado')], default='contacto', max_length=20)), - ('contacto_equipo', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Miembro al cargo')), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')), - ], - ), - ] diff --git a/Plataform_Web/gestion/migrations/0002_remove_patrocinador_contacto_equipo_and_more.py b/Plataform_Web/gestion/migrations/0002_remove_patrocinador_contacto_equipo_and_more.py deleted file mode 100644 index 84fb76c..0000000 --- a/Plataform_Web/gestion/migrations/0002_remove_patrocinador_contacto_equipo_and_more.py +++ /dev/null @@ -1,92 +0,0 @@ -# Generated by Django 6.0.2 on 2026-03-06 19:29 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('gestion', '0001_initial'), - ('temporadas', '0002_temporada_miembros'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name='patrocinador', - name='contacto_equipo', - ), - migrations.RemoveField( - model_name='patrocinador', - name='temporada', - ), - migrations.CreateModel( - name='Gasto', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('concepto', models.CharField(max_length=150)), - ('importe', models.DecimalField(decimal_places=2, max_digits=10)), - ('fecha', models.DateField()), - ('observaciones', models.TextField(blank=True, null=True)), - ('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('e_powertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], max_length=30, verbose_name='Área del gasto')), - ('doc_justificativo', models.FileField(blank=True, null=True, upload_to='contabilidad/gastos/', verbose_name='Ticket o Factura (PDF/IMG)')), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s_registrados', to='temporadas.temporada')), - ], - options={ - 'verbose_name': 'Gasto', - 'verbose_name_plural': 'Gastos', - }, - ), - migrations.CreateModel( - name='Ingreso', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('concepto', models.CharField(max_length=150)), - ('importe', models.DecimalField(decimal_places=2, max_digits=10)), - ('fecha', models.DateField()), - ('observaciones', models.TextField(blank=True, null=True)), - ('categoria', models.CharField(choices=[('patrocinador', 'Patrocinador'), ('donación', 'Donación'), ('premio', 'Premio'), ('recaudación', 'Recaudación'), ('Cuotas', 'Cuotas'), ('Ventas', 'ventas')], max_length=30, verbose_name='Categoria del Ingreso')), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s_registrados', to='temporadas.temporada')), - ], - options={ - 'verbose_name': 'Ingreso', - 'verbose_name_plural': 'Ingresos', - }, - ), - migrations.CreateModel( - name='Patrocinio', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('empresa', models.CharField(max_length=100, verbose_name='Nombre de la Empresa')), - ('persona_contacto', models.CharField(blank=True, max_length=100, null=True, verbose_name='Persona de contacto')), - ('email_contacto', models.EmailField(max_length=254, verbose_name='Correo de contacto')), - ('tipo_patrocinio', models.CharField(choices=[('economico', 'Económico'), ('piezas/materiales', 'Piezas/Materiales'), ('mixto', 'Mixto (Dinero y Piezas)')], max_length=30)), - ('estado', models.CharField(choices=[('en_contacto', 'En contacto'), ('aceptado', 'Aceptado / Activo'), ('denegado', 'Denegado')], default='en_contacto', max_length=20)), - ('importe_economico', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Importe (€)')), - ('fecha_contacto', models.DateField(auto_now_add=True)), - ('contacto_equipo', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patrocinios_gestionados', to=settings.AUTH_USER_MODEL, verbose_name='Persona al cargo del Patrocinio')), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='patrocinios', to='temporadas.temporada', verbose_name='Temporada del patrocinio')), - ], - options={ - 'verbose_name': 'Patrocinio', - 'verbose_name_plural': 'Patrocinios', - }, - ), - migrations.CreateModel( - name='Pieza', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=100, verbose_name='Nombre de la pieza/material')), - ('cantidad', models.PositiveIntegerField(default=1)), - ('patrocinio', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='piezas', to='gestion.patrocinio')), - ], - ), - migrations.DeleteModel( - name='Factura', - ), - migrations.DeleteModel( - name='Patrocinador', - ), - ] diff --git a/Plataform_Web/gestion/migrations/__init__.py b/Plataform_Web/gestion/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/gestion/models.py b/Plataform_Web/gestion/models.py deleted file mode 100644 index 5155e65..0000000 --- a/Plataform_Web/gestion/models.py +++ /dev/null @@ -1,106 +0,0 @@ -from django.db import models -from django.conf import settings -from temporadas.models import Temporada - - -class Patrocinio(models.Model): - - TIPO_PATROCINIO = ( - ('economico', 'Económico'), - ('piezas/materiales', 'Piezas/Materiales'), - ('mixto', 'Mixto (Dinero y Piezas)'), - ) - - ESTADOS = ( - ('en_contacto', 'En contacto'), - ('aceptado', 'Aceptado / Activo'), - ('denegado', 'Denegado'), - ) - - empresa = models.CharField(max_length=100, verbose_name="Nombre de la Empresa") - persona_contacto = models.CharField(max_length=100, blank=True, null=True, verbose_name="Persona de contacto") - email_contacto = models.EmailField(verbose_name="Correo de contacto") - tipo_patrocinio = models.CharField(max_length=30, choices=TIPO_PATROCINIO) - estado = models.CharField(max_length=20, choices=ESTADOS, default='en_contacto') - - # Si el patrocinio es únicamente material, se quedará en 0. Si es económico o mixto, se rellena - importe_economico = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name="Importe (€)") - - fecha_contacto = models.DateField(auto_now_add=True) - # Relaciones - temporada = models.ForeignKey(Temporada, on_delete=models.CASCADE, related_name="patrocinios", verbose_name="Temporada del patrocinio") - contacto_equipo = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="patrocinios_gestionados", verbose_name="Persona al cargo del Patrocinio") - - class Meta: - verbose_name = "Patrocinio" - verbose_name_plural = "Patrocinios" - - def __str__(self): - return f"{self.empresa} ({self.get_estado_display()})" - -class Pieza(models.Model): - nombre = models.CharField(max_length=100, verbose_name="Nombre de la pieza/material") - cantidad = models.PositiveIntegerField(default=1) - - # COMPOSICIÓN ESTRICTA: Si se borra el Patrocinio, se borran las Piezas irremediablemente (CASCADE) - patrocinio = models.ForeignKey(Patrocinio, on_delete=models.CASCADE, related_name="piezas") - - def __str__(self): - return f"{self.cantidad}x {self.nombre} (De: {self.patrocinio.empresa})" - -class Contabilidad(models.Model): - concepto = models.CharField(max_length=150) - importe = models.DecimalField(max_digits=10, decimal_places=2) - fecha = models.DateField() - observaciones = models.TextField(blank=True, null=True) - - # Relación con Temporada. PROTECT evita que borres una temporada si tiene contabilidad asociada (por temas legales) - temporada = models.ForeignKey(Temporada, on_delete=models.PROTECT, related_name="%(class)s_registrados") - - class Meta: - abstract = True #marcamos la clase como abstracta - ordering = ['-fecha'] - - -class Gasto(Contabilidad): - - CATEGORIAS_GASTOS = ( - ('aerodinamica', 'Aerodinámica'), - ('chasis', 'Chasis'), - ('business', 'Business & Operations'), - ('e_powertrain', 'E-Powertrain'), - ('electronica', 'Electrónica'), - ('sdf', 'SDF'), - ('motor_transmision', 'Motor & Transmisión'), - ('software', 'Software'), - ('general', 'General'), - ) - # Hereda concepto, importe, fecha, observaciones y temporada automáticamente - categoria = models.CharField(max_length=30, choices=CATEGORIAS_GASTOS, verbose_name="Área del gasto") - doc_justificativo = models.FileField(upload_to='contabilidad/gastos/', blank=True, null=True, verbose_name="Ticket o Factura (PDF/IMG)") - - class Meta: - verbose_name = "Gasto" - verbose_name_plural = "Gastos" - - def __str__(self): - return f"[-{self.importe}€] {self.concepto} ({self.temporada})" - -class Ingreso(Contabilidad): - CATEGORIAS_INGRESO = ( - ('patrocinador', 'Patrocinador'), - ('donación', 'Donación'), - ('premio', 'Premio'), - ('recaudación', 'Recaudación'), - ('Cuotas', 'Cuotas'), - ('Ventas', 'ventas'), - ) - - # Hereda concepto, importe, fecha, observaciones y temporada automáticamente - categoria = models.CharField(max_length=30, choices=CATEGORIAS_INGRESO, verbose_name="Categoria del Ingreso") - class Meta: - verbose_name = "Ingreso" - verbose_name_plural = "Ingresos" - - def __str__(self): - return f"[+{self.importe}€] {self.concepto} ({self.temporada})" diff --git a/Plataform_Web/gestion/tests.py b/Plataform_Web/gestion/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/Plataform_Web/gestion/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Plataform_Web/gestion/views.py b/Plataform_Web/gestion/views.py deleted file mode 100644 index 8da0530..0000000 --- a/Plataform_Web/gestion/views.py +++ /dev/null @@ -1,202 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_POST -from django.db.models import Sum -from datetime import date - -from temporadas.models import Temporada -from documentos.models import Factura -from users.decorators import require_rol -from .models import Gasto, Ingreso, Patrocinio -from .forms import GastoForm, IngresoForm, PatrocinioForm, PatrocinioEditForm - -# Categorías válidas en Gasto (Documento usa 'normativa' en su lugar de 'general') -_CATEGORIAS_GASTO_VALIDAS = {c[0] for c in Gasto.CATEGORIAS_GASTOS} - - -@login_required -def inicio(request): - temporada_activa = Temporada.objects.filter(actual=True).first() - return render(request, 'index.html', {'temporada_actual': temporada_activa}) - - -@require_rol('directiva') -def contabilidad(request): - temporada_actual = Temporada.objects.filter(actual=True).first() - - gastos = [] - ingresos = [] - facturas_pendientes = [] - total_gastos = 0 - total_ingresos = 0 - presupuesto_inicial = 0 - presupuesto_actual = 0 - - if temporada_actual: - gastos = Gasto.objects.filter(temporada=temporada_actual) - ingresos = Ingreso.objects.filter(temporada=temporada_actual) - facturas_pendientes = Factura.objects.filter( - temporada=temporada_actual, estado='pendiente' - ) - - total_gastos = gastos.aggregate(total=Sum('importe'))['total'] or 0 - total_ingresos = ingresos.aggregate(total=Sum('importe'))['total'] or 0 - presupuesto_inicial = temporada_actual.presupuesto - presupuesto_actual = presupuesto_inicial - total_gastos + total_ingresos - - return render(request, 'contabilidad.html', { - 'temporada_actual': temporada_actual, - 'gastos': gastos, - 'ingresos': ingresos, - 'facturas_pendientes': facturas_pendientes, - 'total_gastos': total_gastos, - 'total_ingresos': total_ingresos, - 'presupuesto_inicial': presupuesto_inicial, - 'presupuesto_actual': presupuesto_actual, - 'gasto_form': GastoForm(), - 'ingreso_form': IngresoForm(), - }) - - -@require_rol('directiva') -@require_POST -def anadir_gasto(request): - temporada_actual = Temporada.objects.filter(actual=True).first() - if not temporada_actual: - messages.error(request, 'No hay temporada activa.') - return redirect('contabilidad') - - form = GastoForm(request.POST) - if form.is_valid(): - gasto = form.save(commit=False) - gasto.fecha = date.today() - gasto.temporada = temporada_actual - gasto.save() - messages.success(request, 'Gasto añadido correctamente.') - else: - messages.error(request, 'Error al añadir el gasto. Revisa los datos.') - return redirect('contabilidad') - - -@require_rol('directiva') -@require_POST -def anadir_ingreso(request): - temporada_actual = Temporada.objects.filter(actual=True).first() - if not temporada_actual: - messages.error(request, 'No hay temporada activa.') - return redirect('contabilidad') - - form = IngresoForm(request.POST) - if form.is_valid(): - ingreso = form.save(commit=False) - ingreso.fecha = date.today() - ingreso.temporada = temporada_actual - ingreso.save() - messages.success(request, 'Ingreso añadido correctamente.') - else: - messages.error(request, 'Error al añadir el ingreso. Revisa los datos.') - return redirect('contabilidad') - - -@require_rol('directiva') -@require_POST -def aceptar_factura(request, pk): - factura = get_object_or_404(Factura, pk=pk) - temporada_actual = Temporada.objects.filter(actual=True).first() - if not temporada_actual: - messages.error(request, 'No hay temporada activa.') - return redirect('contabilidad') - - categoria = factura.categoria if factura.categoria in _CATEGORIAS_GASTO_VALIDAS else 'general' - Gasto.objects.create( - concepto=factura.nombre, - importe=factura.importe, - fecha=date.today(), - categoria=categoria, - temporada=temporada_actual, - observaciones=f'Factura de {factura.empresa}', - doc_justificativo=factura.archivo, - ) - factura.estado = 'aceptada' - factura.save() - messages.success(request, f'Factura de {factura.empresa} aceptada y registrada como gasto.') - return redirect('contabilidad') - - -@require_rol('directiva') -@require_POST -def rechazar_factura(request, pk): - factura = get_object_or_404(Factura, pk=pk) - factura.estado = 'rechazada' - factura.save() - messages.success(request, f'Factura de {factura.empresa} rechazada.') - return redirect('contabilidad') - - -@login_required -def patrocinios(request): - temporada_actual = Temporada.objects.filter(actual=True).first() - pendientes = [] - aceptados = [] - denegados = [] - if temporada_actual: - qs = Patrocinio.objects.filter(temporada=temporada_actual).select_related('contacto_equipo') - pendientes = qs.filter(estado='en_contacto') - aceptados = qs.filter(estado='aceptado') - denegados = qs.filter(estado='denegado') - return render(request, 'patrocinios.html', { - 'temporada_actual': temporada_actual, - 'pendientes': pendientes, - 'aceptados': aceptados, - 'denegados': denegados, - 'form': PatrocinioForm(), - }) - - -@login_required -@require_POST -def proponer_patrocinio(request): - temporada_actual = Temporada.objects.filter(actual=True).first() - if not temporada_actual: - messages.error(request, 'No hay temporada activa. No se puede proponer un patrocinio.') - return redirect('patrocinios') - - form = PatrocinioForm(request.POST) - if form.is_valid(): - empresa = form.cleaned_data['empresa'].strip() - if Patrocinio.objects.filter(empresa__iexact=empresa, temporada=temporada_actual).exists(): - messages.error(request, f'Ya existe un patrocinio con "{empresa}" en la temporada actual.') - else: - patrocinio = form.save(commit=False) - patrocinio.estado = 'en_contacto' - patrocinio.temporada = temporada_actual - patrocinio.contacto_equipo = request.user - patrocinio.save() - messages.success(request, f'Patrocinio de "{empresa}" propuesto correctamente.') - else: - messages.error(request, 'Error en el formulario. Revisa los datos.') - return redirect('patrocinios') - - -@require_rol('directiva') -def editar_patrocinio(request, pk): - patrocinio = get_object_or_404(Patrocinio, pk=pk) - form = PatrocinioEditForm(request.POST or None, instance=patrocinio) - if request.method == 'POST' and form.is_valid(): - form.save() - messages.success(request, 'Patrocinio actualizado correctamente.') - return redirect('patrocinios') - return render(request, 'editar_patrocinio.html', {'form': form, 'patrocinio': patrocinio}) - - -@require_rol('directiva') -@require_POST -def cambiar_estado_patrocinio(request, pk): - patrocinio = get_object_or_404(Patrocinio, pk=pk) - nuevo_estado = request.POST.get('estado') - if nuevo_estado in ('aceptado', 'denegado', 'en_contacto'): - patrocinio.estado = nuevo_estado - patrocinio.save() - messages.success(request, f'Estado de "{patrocinio.empresa}" actualizado.') - return redirect('patrocinios') diff --git a/Plataform_Web/manage.py b/Plataform_Web/manage.py deleted file mode 100644 index d8daff8..0000000 --- a/Plataform_Web/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gades_manager.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/Plataform_Web/media/ingenieria_docs/PruebaTest1.png b/Plataform_Web/media/ingenieria_docs/PruebaTest1.png deleted file mode 100644 index 0c824e6..0000000 Binary files a/Plataform_Web/media/ingenieria_docs/PruebaTest1.png and /dev/null differ diff --git a/Plataform_Web/pruebas/__init__.py b/Plataform_Web/pruebas/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/pruebas/admin.py b/Plataform_Web/pruebas/admin.py deleted file mode 100644 index c2bb061..0000000 --- a/Plataform_Web/pruebas/admin.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.contrib import admin -from .models import Prueba, Telemetria, Variable - -# INLINES -# Usamos inlines que nos sirve para cuando se este creando un registro de Telemetría, podremos añadir sus Variables en la misma pantalla sin tener que ir a otro menú -class VariableInline(admin.TabularInline): - model = Variable - extra = 1 - -class TelemetriaInline(admin.TabularInline): - model = Telemetria - extra = 0 - -# PANELES DE ADMINISTRACIÓN -@admin.register(Prueba) -class PruebaAdmin(admin.ModelAdmin): - list_display = ('nombre', 'categoria', 'fecha_inicio', 'temporada', 'realizado_por') - - # Filtros para encontrar los test dado una temporada y una área técnica - list_filter = ('categoria', 'temporada') - - # Buscador de texto - search_fields = ('nombre', 'descripcion', 'resultados') - - # Una barra de navegación en la parte superior, para poder hacer clic en un año y en un mes y mostrar todos los datos de ese me - date_hierarchy = 'fecha_inicio' - - # Mostramos los archivos de telemetría directamente dentro del test - inlines = [TelemetriaInline] - - -@admin.register(Telemetria) -class TelemetriaAdmin(admin.ModelAdmin): - list_display = ('nombre', 'prueba', 'fecha_subida') - - # Filtra test de telemetría basandose en la temporada o área tecnica de su test correspondiente - list_filter = ('prueba__temporada', 'prueba__categoria') - - # Buscador del nombre de la telemetría o por el nombre de la prueba - search_fields = ('nombre', 'prueba__nombre') - - # Mostramos las variables de su telemetría - inlines = [VariableInline] - - -@admin.register(Variable) -class VariableAdmin(admin.ModelAdmin): - # Aunque se pueden crear desde el Inline, dejamos su tabla propia por si fuese necesario, por prevenir - list_display = ('nombre', 'unidad_medida', 'telemetria') - search_fields = ('nombre', 'telemetria__nombre') \ No newline at end of file diff --git a/Plataform_Web/pruebas/apps.py b/Plataform_Web/pruebas/apps.py deleted file mode 100644 index 8cbb027..0000000 --- a/Plataform_Web/pruebas/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class PruebasConfig(AppConfig): - name = 'pruebas' diff --git a/Plataform_Web/pruebas/forms.py b/Plataform_Web/pruebas/forms.py deleted file mode 100644 index 60029e9..0000000 --- a/Plataform_Web/pruebas/forms.py +++ /dev/null @@ -1,22 +0,0 @@ -from django import forms -from .models import Prueba, Telemetria - - -class PruebaForm(forms.ModelForm): - class Meta: - model = Prueba - fields = ['nombre', 'descripcion', 'fecha_inicio', 'fecha_fin', 'categoria', 'resultados', 'temporada'] - widgets = { - 'fecha_inicio': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), - 'fecha_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), - } - - -class TelemetriaForm(forms.ModelForm): - class Meta: - model = Telemetria - fields = ['nombre', 'archivo_csv'] - widgets = { - 'nombre': forms.TextInput(attrs={'class': 'form-control form-control-sm'}), - 'archivo_csv': forms.ClearableFileInput(attrs={'class': 'form-control form-control-sm'}), - } diff --git a/Plataform_Web/pruebas/migrations/0001_initial.py b/Plataform_Web/pruebas/migrations/0001_initial.py deleted file mode 100644 index d531753..0000000 --- a/Plataform_Web/pruebas/migrations/0001_initial.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 6.0.2 on 2026-02-19 10:14 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('temporadas', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='TestGeneral', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=150, verbose_name='Nombre del Test')), - ('descripcion', models.TextField(verbose_name='Objetivo / Descripción del test')), - ('fecha_inicio', models.DateField()), - ('fecha_fin', models.DateField()), - ('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('general', 'General')], default='general', max_length=30)), - ('resultados', models.TextField(blank=True, null=True, verbose_name='Conclusiones y Resultados')), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='temporadas.temporada')), - ], - options={ - 'verbose_name': 'Test General', - 'verbose_name_plural': 'Tests Generales', - 'ordering': ['-fecha_inicio'], - }, - ), - ] diff --git a/Plataform_Web/pruebas/migrations/0002_prueba_telemetria_variable_delete_testgeneral.py b/Plataform_Web/pruebas/migrations/0002_prueba_telemetria_variable_delete_testgeneral.py deleted file mode 100644 index 8bc4e3b..0000000 --- a/Plataform_Web/pruebas/migrations/0002_prueba_telemetria_variable_delete_testgeneral.py +++ /dev/null @@ -1,67 +0,0 @@ -# Generated by Django 6.0.2 on 2026-03-06 19:29 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('pruebas', '0001_initial'), - ('temporadas', '0002_temporada_miembros'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Prueba', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=150, verbose_name='Nombre del Test')), - ('descripcion', models.TextField(verbose_name='Objetivo / Descripción de la prueba')), - ('fecha_inicio', models.DateField()), - ('fecha_fin', models.DateField()), - ('categoria', models.CharField(choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software'), ('general', 'General')], default='general', max_length=30)), - ('resultados', models.TextField(blank=True, null=True, verbose_name='Conclusiones y Resultados')), - ('realizado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pruebas_realizadas', to=settings.AUTH_USER_MODEL, verbose_name='Realizado por')), - ('temporada', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pruebas', to='temporadas.temporada')), - ], - options={ - 'verbose_name': 'Prueba', - 'verbose_name_plural': 'Pruebas', - 'ordering': ['-fecha_inicio'], - }, - ), - migrations.CreateModel( - name='Telemetria', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=100, verbose_name='Nombre del registro')), - ('archivo_csv', models.FileField(upload_to='telemetria/archivos_csv/', verbose_name='Archivo de Datos (CSV)')), - ('fecha_subida', models.DateTimeField(auto_now_add=True)), - ('prueba', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='archivos_telemetria', to='pruebas.prueba')), - ], - options={ - 'verbose_name': 'Registro de Telemetría', - 'verbose_name_plural': 'Registros de Telemetría', - }, - ), - migrations.CreateModel( - name='Variable', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=50, verbose_name='Nombre ')), - ('unidad_medida', models.CharField(blank=True, max_length=20, null=True, verbose_name='Unidad de medida')), - ('descripcion', models.TextField(blank=True, null=True)), - ('telemetria', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variables', to='pruebas.telemetria')), - ], - options={ - 'verbose_name': 'Variable de Telemetría', - 'verbose_name_plural': 'Variables de Telemetría', - }, - ), - migrations.DeleteModel( - name='TestGeneral', - ), - ] diff --git a/Plataform_Web/pruebas/migrations/__init__.py b/Plataform_Web/pruebas/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/pruebas/models.py b/Plataform_Web/pruebas/models.py deleted file mode 100644 index 257563e..0000000 --- a/Plataform_Web/pruebas/models.py +++ /dev/null @@ -1,78 +0,0 @@ -from django.db import models -from django.conf import settings -from temporadas.models import Temporada -import os - -class Prueba(models.Model): - CATEGORIAS_TEST = ( - ('aerodinamica', 'Aerodinámica'), - ('chasis', 'Chasis'), - ('epowertrain', 'E-Powertrain'), - ('electronica', 'Electrónica'), - ('sdf', 'SDF'), - ('motor_transmision', 'Motor & Transmisión'), - ('software', 'Software'), - ('general', 'General'), - ) - - nombre = models.CharField(max_length=150, verbose_name="Nombre del Test") - descripcion = models.TextField(verbose_name="Objetivo / Descripción de la prueba") - - fecha_inicio = models.DateField() - fecha_fin = models.DateField() - - categoria = models.CharField(max_length=30, choices=CATEGORIAS_TEST, default='general') - resultados = models.TextField(blank=True, null=True, verbose_name="Conclusiones y Resultados") - - # Relaciones - # Relacion con Temporada - temporada = models.ForeignKey(Temporada, on_delete=models.CASCADE, related_name="pruebas") - #Relacion con Usuario - realizado_por = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="pruebas_realizadas", verbose_name="Realizado por") - - class Meta: - verbose_name = "Prueba" - verbose_name_plural = "Pruebas" - ordering = ['-fecha_inicio'] - - def __str__(self): - return f"{self.nombre} ({self.fecha_inicio})" - -class Telemetria(models.Model): - nombre = models.CharField(max_length=100, verbose_name="Nombre del registro") - archivo_csv = models.FileField(upload_to='telemetria/archivos_csv/', verbose_name="Archivo de Datos (CSV)") - fecha_subida = models.DateTimeField(auto_now_add=True) - - # La telemetría se obtiene durante una Prueba en pista - prueba = models.ForeignKey(Prueba, on_delete=models.CASCADE, related_name="archivos_telemetria") - - class Meta: - verbose_name = "Registro de Telemetría" - verbose_name_plural = "Registros de Telemetría" - - def __str__(self): - return f"Telemetría: {self.nombre} (De: {self.prueba.nombre})" - - def delete(self, *args, **kwargs): - # Borra el archivo físico del disco duro cuando borras la entrada en la base de datos - if self.archivo_csv: - if os.path.isfile(self.archivo_csv.path): - os.remove(self.archivo_csv.path) - super().delete(*args, **kwargs) - - -class Variable(models.Model): - nombre = models.CharField(max_length=50, verbose_name="Nombre ") - unidad_medida = models.CharField(max_length=20, verbose_name="Unidad de medida", blank=True, null=True) - descripcion = models.TextField(blank=True, null=True) - - # COMPOSICIÓN - telemetria = models.ForeignKey(Telemetria, on_delete=models.CASCADE, related_name="variables") - - class Meta: - verbose_name = "Variable de Telemetría" - verbose_name_plural = "Variables de Telemetría" - - def __str__(self): - unidad = f" [{self.unidad_medida}]" if self.unidad_medida else "" - return f"{self.nombre}{unidad} (De: {self.telemetria.nombre})" \ No newline at end of file diff --git a/Plataform_Web/pruebas/tests.py b/Plataform_Web/pruebas/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/Plataform_Web/pruebas/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Plataform_Web/pruebas/views.py b/Plataform_Web/pruebas/views.py deleted file mode 100644 index 38fcb78..0000000 --- a/Plataform_Web/pruebas/views.py +++ /dev/null @@ -1,102 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_POST -from users.decorators import require_rol -from temporadas.models import Temporada -from .models import Prueba -from .forms import PruebaForm, TelemetriaForm - - -def puede_subir_csv(user): - return user.rol == 'directiva' or user.especialidad == 'software' - - -@login_required -def listado_pruebas(request): - temporada_actual = Temporada.objects.filter(actual=True).first() - temporada_id = request.GET.get('temporada') or (temporada_actual.pk if temporada_actual else None) - categoria = request.GET.get('categoria') - - pruebas = Prueba.objects.all() - if temporada_id: - pruebas = pruebas.filter(temporada_id=temporada_id) - if categoria: - pruebas = pruebas.filter(categoria=categoria) - - return render(request, 'listado_pruebas.html', { - 'pruebas': pruebas, - 'temporadas': Temporada.objects.all(), - 'categorias': Prueba.CATEGORIAS_TEST, - 'temporada_seleccionada': str(temporada_id) if temporada_id else '', - 'categoria_seleccionada': categoria or '', - }) - - -@login_required -def detalle_prueba(request, pk): - prueba = get_object_or_404(Prueba, pk=pk) - puede_editar = request.user.rol == 'directiva' or ( - request.user.rol == 'jefe_area' and prueba.realizado_por_id == request.user.id - ) - return render(request, 'detalle_prueba.html', { - 'prueba': prueba, - 'puede_editar': puede_editar, - 'puede_subir_csv': puede_subir_csv(request.user), - 'form_csv': TelemetriaForm(), - }) - - -@require_rol('directiva', 'jefe_area') -def crear_prueba(request): - form = PruebaForm(request.POST or None) - if request.method == 'POST' and form.is_valid(): - prueba = form.save(commit=False) - prueba.realizado_por = request.user - prueba.save() - messages.success(request, 'Test creado correctamente.') - return redirect('listado_pruebas') - return render(request, 'editar_prueba.html', {'form': form, 'prueba': None}) - - -@require_rol('directiva', 'jefe_area') -def editar_prueba(request, pk): - prueba = get_object_or_404(Prueba, pk=pk) - if request.user.rol == 'jefe_area' and prueba.realizado_por_id != request.user.id: - messages.error(request, 'Solo puedes editar los tests que tú has creado.') - return redirect('detalle_prueba', pk=prueba.pk) - - form = PruebaForm(request.POST or None, instance=prueba) - if request.method == 'POST' and form.is_valid(): - form.save() - messages.success(request, 'Test actualizado correctamente.') - return redirect('detalle_prueba', pk=prueba.pk) - return render(request, 'editar_prueba.html', {'form': form, 'prueba': prueba}) - - -@require_rol('directiva') -@require_POST -def eliminar_prueba(request, pk): - prueba = get_object_or_404(Prueba, pk=pk) - prueba.delete() - messages.success(request, f'Test "{prueba.nombre}" eliminado.') - return redirect('listado_pruebas') - - -@login_required -@require_POST -def subir_csv(request, pk): - prueba = get_object_or_404(Prueba, pk=pk) - if not puede_subir_csv(request.user): - messages.error(request, 'No tienes permiso para subir archivos de telemetría.') - return redirect('detalle_prueba', pk=prueba.pk) - - form = TelemetriaForm(request.POST, request.FILES) - if form.is_valid(): - telemetria = form.save(commit=False) - telemetria.prueba = prueba - telemetria.save() - messages.success(request, 'Archivo de telemetría subido correctamente.') - else: - messages.error(request, 'No se pudo subir el archivo. Revisa el formulario.') - return redirect('detalle_prueba', pk=prueba.pk) diff --git a/Plataform_Web/static/dossiers/DossierEN_2025_2026.pdf b/Plataform_Web/static/dossiers/DossierEN_2025_2026.pdf deleted file mode 100644 index e77e215..0000000 Binary files a/Plataform_Web/static/dossiers/DossierEN_2025_2026.pdf and /dev/null differ diff --git a/Plataform_Web/static/dossiers/Dossier_2025_2026.pdf b/Plataform_Web/static/dossiers/Dossier_2025_2026.pdf deleted file mode 100644 index cc1b2f1..0000000 Binary files a/Plataform_Web/static/dossiers/Dossier_2025_2026.pdf and /dev/null differ diff --git a/Plataform_Web/static/images/logo_gades.png b/Plataform_Web/static/images/logo_gades.png deleted file mode 100644 index 36e0782..0000000 Binary files a/Plataform_Web/static/images/logo_gades.png and /dev/null differ diff --git a/Plataform_Web/static/images/monoplaza_gades.jpg b/Plataform_Web/static/images/monoplaza_gades.jpg deleted file mode 100644 index fe02155..0000000 Binary files a/Plataform_Web/static/images/monoplaza_gades.jpg and /dev/null differ diff --git a/Plataform_Web/templates/base.html b/Plataform_Web/templates/base.html deleted file mode 100644 index fd7817d..0000000 --- a/Plataform_Web/templates/base.html +++ /dev/null @@ -1,111 +0,0 @@ -{% load static %} - - - - - - {% block title %}Intranet | Formula Gades{% endblock title %} - - - - - - {% block extra_head %}{% endblock extra_head %} - - - - - - -
- {% block content %} - {% endblock content %} -
- -
-

© 2026 Formula Gades - Universidad de Cádiz. Todos los derechos reservados.

-
- - - - {% block extra_js %}{% endblock extra_js %} - - \ No newline at end of file diff --git a/Plataform_Web/templates/contabilidad.html b/Plataform_Web/templates/contabilidad.html deleted file mode 100644 index 9beeaf4..0000000 --- a/Plataform_Web/templates/contabilidad.html +++ /dev/null @@ -1,247 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Contabilidad | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

Contabilidad

-

- {% if temporada_actual %}{{ temporada_actual.nombre }}{% else %}Sin temporada activa{% endif %} -

-
-
-
- -{% if messages %} -
-
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
-
-{% endif %} - -{% if not temporada_actual %} -
-
-
No hay ninguna temporada activa. Activa una temporada desde Gestión de Temporadas.
-
-
-{% else %} - -{# ── Bloque 1: 4 tarjetas resumen ── #} -
-
-
-
-

Presupuesto Inicial

-

{{ presupuesto_inicial|floatformat:2 }} €

-
-
-
-
-
-
-

Total Ingresos

-

+{{ total_ingresos|floatformat:2 }} €

-
-
-
-
-
-
-

Total Gastos

-

-{{ total_gastos|floatformat:2 }} €

-
-
-
-
-
-
-

Presupuesto Actual

-

- {{ presupuesto_actual|floatformat:2 }} € -

-
-
-
-
- -{# ── Bloque 2: Gastos | Ingresos ── #} -
- - {# Columna Gastos #} -
-
-
-
-
Gastos
- -
-
- {% if gastos %} -
    - {% for gasto in gastos %} -
  • -
    -
    {{ gasto.concepto }}
    -
    {{ gasto.get_categoria_display }} · {{ gasto.fecha|date:"d/m/Y" }}
    -
    - -{{ gasto.importe|floatformat:2 }} € -
  • - {% endfor %} -
- {% else %} -

Sin gastos registrados.

- {% endif %} -
-
-
-
- - {# Columna Ingresos #} -
-
-
-
-
Ingresos
- -
-
- {% if ingresos %} -
    - {% for ingreso in ingresos %} -
  • -
    -
    {{ ingreso.concepto }}
    -
    {{ ingreso.get_categoria_display }} · {{ ingreso.fecha|date:"d/m/Y" }}
    -
    - +{{ ingreso.importe|floatformat:2 }} € -
  • - {% endfor %} -
- {% else %} -

Sin ingresos registrados.

- {% endif %} -
-
-
-
-
- -{# ── Bloque 3: Facturas pendientes ── #} -
-
-
-
-
Facturas Pendientes
-
- - - - - - - - - - - - {% for factura in facturas_pendientes %} - - - - - - - - {% empty %} - - - - {% endfor %} - -
NombreEmpresaImporteCategoríaAcciones
{{ factura.nombre }}{{ factura.empresa }}{{ factura.importe|floatformat:2 }} €{{ factura.get_categoria_display }} -
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-
No hay facturas pendientes.
-
-
-
-
-
- -{% endif %}{# end if temporada_actual #} - -{# ── Modal Añadir Gasto ── #} - - -{# ── Modal Añadir Ingreso ── #} - - -{% endblock content %} diff --git a/Plataform_Web/templates/detalle_prueba.html b/Plataform_Web/templates/detalle_prueba.html deleted file mode 100644 index 1a8d889..0000000 --- a/Plataform_Web/templates/detalle_prueba.html +++ /dev/null @@ -1,112 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}{{ prueba.nombre }} | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-
-

{{ prueba.nombre }}

-

{{ prueba.get_categoria_display }} · Temporada {{ prueba.temporada.nombre }}

-
-
- {% if puede_editar %} - Editar - {% endif %} - {% if user.rol == 'directiva' %} - - {% endif %} -
-
-
-
- -
- - {% if messages %} -
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} - -
-
-
-
Objetivo / Descripción
-

{{ prueba.descripcion }}

- -
-
Fecha inicio: {{ prueba.fecha_inicio|date:"d/m/Y" }}
-
Fecha fin: {{ prueba.fecha_fin|date:"d/m/Y" }}
-
-

Realizado por: {{ prueba.realizado_por.first_name|default:"—" }}

- - {% if prueba.resultados %} -
Conclusiones y Resultados
-

{{ prueba.resultados }}

- {% endif %} -
-
-
- -
-
-
-
Archivos de telemetría
-
    - {% for telemetria in prueba.archivos_telemetria.all %} -
  • - {{ telemetria.nombre }} - {{ telemetria.fecha_subida|date:"d/m/Y" }} -
  • - {% empty %} -
  • No hay archivos subidos todavía.
  • - {% endfor %} -
- - {% if puede_subir_csv %} -
- {% csrf_token %} -
- - {{ form_csv.nombre }} -
-
- - {{ form_csv.archivo_csv }} -
- -
- {% endif %} -
-
-
-
- -{% if user.rol == 'directiva' %} - -{% endif %} -{% endblock content %} diff --git a/Plataform_Web/templates/editar_patrocinio.html b/Plataform_Web/templates/editar_patrocinio.html deleted file mode 100644 index 0b7d0bc..0000000 --- a/Plataform_Web/templates/editar_patrocinio.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Editar Patrocinio | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

Editar Patrocinio

-

{{ patrocinio.empresa }}

-
-
-
- -
-
-
-
-
Datos del Patrocinio
- -
- {% csrf_token %} - - {% for field in form %} -
- - {{ field }} - {% for error in field.errors %} -
{{ error }}
- {% endfor %} -
- {% endfor %} - -
- - Cancelar -
-
-
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/editar_prueba.html b/Plataform_Web/templates/editar_prueba.html deleted file mode 100644 index 1e7e012..0000000 --- a/Plataform_Web/templates/editar_prueba.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}{% if prueba %}Editar Test{% else %}Nuevo Test{% endif %} | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

{% if prueba %}Editar Test{% else %}Nuevo Test{% endif %}

-

{% if prueba %}{{ prueba.nombre }}{% else %}Rellena los datos del nuevo test.{% endif %}

-
-
-
- -
-
-
-
- -
- {% csrf_token %} - -
- - - {% for error in form.nombre.errors %} -
{{ error }}
- {% endfor %} -
- -
- - - {% for error in form.descripcion.errors %} -
{{ error }}
- {% endfor %} -
- -
-
- - {{ form.fecha_inicio }} - {% for error in form.fecha_inicio.errors %} -
{{ error }}
- {% endfor %} -
-
- - {{ form.fecha_fin }} - {% for error in form.fecha_fin.errors %} -
{{ error }}
- {% endfor %} -
-
- -
-
- - -
-
- - - {% for error in form.temporada.errors %} -
{{ error }}
- {% endfor %} -
-
- -
- - - {% for error in form.resultados.errors %} -
{{ error }}
- {% endfor %} -
- -
- - Cancelar -
-
- -
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/editar_temporada.html b/Plataform_Web/templates/editar_temporada.html deleted file mode 100644 index cd7d320..0000000 --- a/Plataform_Web/templates/editar_temporada.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}{% if temporada %}Editar Temporada{% else %}Nueva Temporada{% endif %} | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

{% if temporada %}Editar Temporada{% else %}Nueva Temporada{% endif %}

-

{% if temporada %}{{ temporada.nombre }}{% else %}Rellena los datos de la nueva temporada.{% endif %}

-
-
-
- -
-
-
-
- -
- {% csrf_token %} - -
- - - {% for error in form.nombre.errors %} -
{{ error }}
- {% endfor %} -
- -
-
- - {{ form.fecha_inicio }} - {% for error in form.fecha_inicio.errors %} -
{{ error }}
- {% endfor %} -
-
- - {{ form.fecha_fin }} - {% for error in form.fecha_fin.errors %} -
{{ error }}
- {% endfor %} -
-
- -
- - - {% for error in form.presupuesto.errors %} -
{{ error }}
- {% endfor %} -
- -
- - -
Solo puede haber una temporada activa. Al marcar esta se desactivarán las demás automáticamente.
-
- -
- -
- {% for checkbox in form.miembros %} -
- {{ checkbox.tag }} - -
- {% empty %} - No hay usuarios en el sistema. - {% endfor %} -
-
- -
- - Cancelar -
-
- -
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/editar_usuario.html b/Plataform_Web/templates/editar_usuario.html deleted file mode 100644 index 9e107d8..0000000 --- a/Plataform_Web/templates/editar_usuario.html +++ /dev/null @@ -1,70 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Editar Usuario | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

Editar Usuario

-

{{ usuario.username }}

-
-
-
- -
-
-
-
-
Datos del Usuario
- -
- {% csrf_token %} - -
- - {{ form.username }} - {% for error in form.username.errors %} -
{{ error }}
- {% endfor %} -
- -
-
- - {{ form.first_name }} -
-
- - {{ form.last_name }} -
-
- -
- - {{ form.email }} - {% for error in form.email.errors %} -
{{ error }}
- {% endfor %} -
- -
-
- - {{ form.rol }} -
-
- - {{ form.especialidad }} -
-
- - - Cancelar -
-
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/gestion_temporadas.html b/Plataform_Web/templates/gestion_temporadas.html deleted file mode 100644 index 404f3b9..0000000 --- a/Plataform_Web/templates/gestion_temporadas.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Gestión de Temporadas | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-
-

Gestión de Temporadas

-

Crea, edita y elimina temporadas del equipo.

-
- + Nueva Temporada -
-
-
- -
- - {% if messages %} -
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} - -
-
-
- -
- - - - - - - - - - - - - - {% for temporada in temporadas %} - - - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
NombreFecha inicioFecha finPresupuestoMiembrosEstadoAcciones
{{ temporada.nombre }}{{ temporada.fecha_inicio|date:"d/m/Y" }}{{ temporada.fecha_fin|date:"d/m/Y" }}{{ temporada.presupuesto|floatformat:2 }} €{{ temporada.miembros.count }} - {% if temporada.actual %} - Activa - {% else %} - Anterior - {% endif %} - - Editar - -
No hay temporadas creadas todavía.
-
- -
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/gestion_usuarios.html b/Plataform_Web/templates/gestion_usuarios.html deleted file mode 100644 index 312d10e..0000000 --- a/Plataform_Web/templates/gestion_usuarios.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Gestión de Usuarios | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

Gestión de Usuarios

-

Administra las cuentas del sistema: edita o elimina usuarios.

-
-
-
- -
- - {% if messages %} -
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} - -
-
-
- -
-
- - -
- -
- - -
- -
- - -
- -
- - {% if filtro_rol or filtro_especialidad or filtro_apellido %} - × - {% endif %} -
-
- -
- - - - - - - - - - - - - - {% for usuario in usuarios %} - - - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
UsuarioNombreApellidosEmailÁrea TécnicaRolAcciones
{{ usuario.username }}{{ usuario.first_name }}{{ usuario.last_name }}{{ usuario.email }}{{ usuario.get_especialidad_display|default:"—" }}{{ usuario.get_rol_display }} - Editar - -
No se han encontrado usuarios con los filtros seleccionados.
-
- -
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/index.html b/Plataform_Web/templates/index.html deleted file mode 100644 index a3a931a..0000000 --- a/Plataform_Web/templates/index.html +++ /dev/null @@ -1,115 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Inicio | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-
-

Bienvenido, {{ user.first_name }} {{ user.last_name }}

-

Panel de control de la plataforma de gestión interna.

-
-
- - Temporada Activa: {% if temporada_actual %}{{ temporada_actual.nombre }}{% else %}Ninguna activa{% endif %} - -
-
-
-
- -
- -
-
- Monoplaza Formula Gades -
-
- -
-
- - {% if user.rol != 'directiva' %} - -
-
-
-
Mi Área Técnica
-

Acceso a documentación, planos y buzón de compras de tu departamento.

- Acceder a mi Área -
-
-
- -
-
-
-
Pruebas y Telemetría
-

Carga de archivos de registros CSV y procesamiento analítico de telemetría.

- Ver Telemetría -
-
-
- -
-
-
-
Miembros del Equipo
-

Directorio completo de integrantes, correos corporativos y roles.

- Ver Directorio -
-
-
- - {% else %} - -
-
-
-
Pruebas y Telemetría
-

Carga de archivos de registros CSV y procesamiento analítico de telemetría.

- Ver Telemetría -
-
-
- -
-
-
-
Miembros del Equipo
-

Directorio completo de integrantes, correos corporativos y roles.

- Ver Directorio -
-
-
- -
-
-
-
Contabilidad Global
-

Supervisión del presupuesto anual y control del buzón financiero de facturas.

- Gestionar Finanzas -
-
-
- -
-
-
-
Administración del Sistema
-

Módulo avanzado de configuración de permisos, usuarios y temporadas.

- Panel de Control -
-
-
- - {% endif %} - -
-
- -
-{% endblock content %} \ No newline at end of file diff --git a/Plataform_Web/templates/listado_miembros.html b/Plataform_Web/templates/listado_miembros.html deleted file mode 100644 index 5746c3a..0000000 --- a/Plataform_Web/templates/listado_miembros.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Miembros | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

Miembros del Equipo

-

Directorio completo de integrantes de Formula Gades.

-
-
-
- -
-
-
-
- -
-
- - -
- -
- - -
- -
- - -
- -
- - {% if filtro_rol or filtro_especialidad or filtro_apellido %} - × - {% endif %} -
-
- -
- - - - - - - - - - - - - {% for miembro in miembros %} - - - - - - - - - {% empty %} - - - - {% endfor %} - -
UsuarioNombreApellidosEmailÁrea TécnicaRol
{{ miembro.username }}{{ miembro.first_name }}{{ miembro.last_name }}{{ miembro.email }}{{ miembro.get_especialidad_display|default:"—" }}{{ miembro.get_rol_display }}
No se han encontrado miembros con los filtros seleccionados.
-
- -
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/listado_pruebas.html b/Plataform_Web/templates/listado_pruebas.html deleted file mode 100644 index 7741beb..0000000 --- a/Plataform_Web/templates/listado_pruebas.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Pruebas | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-
-

Pruebas

-

Tests realizados durante la temporada.

-
- {% if user.rol == 'directiva' or user.rol == 'jefe_area' %} - + Nuevo Test - {% endif %} -
-
-
- -
- - {% if messages %} -
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} - -
-
-
- -
-
- - -
-
- - -
-
- -
-
- -
- - - - - - - - - - - - {% for prueba in pruebas %} - - - - - - - - {% empty %} - - - - {% endfor %} - -
NombreFecha inicioFecha finCategoríaRealizado por
{{ prueba.nombre }}{{ prueba.fecha_inicio|date:"d/m/Y" }}{{ prueba.fecha_fin|date:"d/m/Y" }}{{ prueba.get_categoria_display }}{{ prueba.realizado_por.first_name|default:"—" }}
No hay tests todavía.
-
- -
-
-
-
-{% endblock content %} diff --git a/Plataform_Web/templates/login.html b/Plataform_Web/templates/login.html deleted file mode 100644 index 11950dd..0000000 --- a/Plataform_Web/templates/login.html +++ /dev/null @@ -1,97 +0,0 @@ -{% load static %} - - - - - - Iniciar Sesión | Formula Gades - - - - - - - - - - - - diff --git a/Plataform_Web/templates/mi_perfil.html b/Plataform_Web/templates/mi_perfil.html deleted file mode 100644 index 2b73724..0000000 --- a/Plataform_Web/templates/mi_perfil.html +++ /dev/null @@ -1,121 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Mi Perfil | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-

Mi Perfil

-

Consulta y edita tus datos personales.

-
-
-
- -
- - {% if messages %} -
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
- {% endif %} - -
-
-
-
Datos Personales
- -
-
- - -
-
- - -
-
- -
- {% csrf_token %} - -
- - {{ perfil_form.username }} - {% for error in perfil_form.username.errors %} -
{{ error }}
- {% endfor %} -
- -
-
- - {{ perfil_form.first_name }} -
-
- - {{ perfil_form.last_name }} -
-
- -
- - {{ perfil_form.email }} - {% for error in perfil_form.email.errors %} -
{{ error }}
- {% endfor %} -
- - -
-
-
-
- -
-
-
-
Cambiar Contraseña
- -
- {% csrf_token %} - -
- - - {% for error in password_form.old_password.errors %} -
{{ error }}
- {% endfor %} -
- -
- - - {% for error in password_form.new_password1.errors %} -
{{ error }}
- {% endfor %} -
- -
- - - {% for error in password_form.new_password2.errors %} -
{{ error }}
- {% endfor %} -
- - {% if password_form.non_field_errors %} -
{{ password_form.non_field_errors }}
- {% endif %} - - -
-
-
-
- -
-{% endblock content %} diff --git a/Plataform_Web/templates/patrocinios.html b/Plataform_Web/templates/patrocinios.html deleted file mode 100644 index 189321b..0000000 --- a/Plataform_Web/templates/patrocinios.html +++ /dev/null @@ -1,269 +0,0 @@ -{% extends 'base.html' %} -{% load static %} - -{% block title %}Patrocinios | Gades Manager{% endblock title %} - -{% block content %} -
-
-
-
-

Patrocinios

-

Dossiers y gestión de patrocinadores del equipo.

-
- -
-
-
- -{% if messages %} -
-
- {% for message in messages %} -
{{ message }}
- {% endfor %} -
-
-{% endif %} - -
- - {# ── Bloque 1: Dossiers PDF ── #} -
-
-
-
- - - - -
-
Dossier de Patrocinio
-

Versión en español · Temporada 2025/2026

- -
-
-
- -
-
-
-
- - - - -
-
Sponsorship Dossier
-

English version · Season 2025/2026

-
- Open - Download -
-
-
-
- - {# ── Bloque 2: Patrocinios agrupados ── #} -
- - {% if not temporada_actual %} -
No hay temporada activa. Activa una desde Gestión de Temporadas.
- {% else %} - - {# Pendientes #} -
-
- {{ pendientes.count }} -
Pendientes / En contacto
-
-
-
- - - - - - - - - - {% if user.rol == 'directiva' %}{% endif %} - - - - {% for p in pendientes %} - - - - - - - - {% if user.rol == 'directiva' %} - - {% endif %} - - {% empty %} - - {% endfor %} - -
EmpresaTipoPersona contactoEmailPropuesto porFechaAcciones
{{ p.empresa }}{{ p.get_tipo_patrocinio_display }}{{ p.persona_contacto|default:"—" }}{{ p.email_contacto }}{{ p.contacto_equipo.get_full_name|default:p.contacto_equipo }}{{ p.fecha_contacto|date:"d/m/Y" }} -
-
- {% csrf_token %} - - -
-
- {% csrf_token %} - - -
- Editar -
-
No hay patrocinios pendientes.
-
-
-
- - {# Aceptados #} -
-
- {{ aceptados.count }} -
Aceptados
-
-
-
- - - - - - - - - - {% if user.rol == 'directiva' %}{% endif %} - - - - {% for p in aceptados %} - - - - - - - - {% if user.rol == 'directiva' %} - - {% endif %} - - {% empty %} - - {% endfor %} - -
EmpresaTipoImportePersona contactoEmailFechaAcciones
{{ p.empresa }}{{ p.get_tipo_patrocinio_display }}{{ p.importe_economico|floatformat:2 }} €{{ p.persona_contacto|default:"—" }}{{ p.email_contacto }}{{ p.fecha_contacto|date:"d/m/Y" }} -
-
- {% csrf_token %} - - -
- Editar -
-
No hay patrocinios aceptados.
-
-
-
- - {# Rechazados — colapsado por defecto #} -
-
- {{ denegados.count }} -
Rechazados
- -
-
-
- - - - - - - - - {% if user.rol == 'directiva' %}{% endif %} - - - - {% for p in denegados %} - - - - - - - {% if user.rol == 'directiva' %} - - {% endif %} - - {% empty %} - - {% endfor %} - -
EmpresaTipoPersona contactoEmailFechaAcciones
{{ p.empresa }}{{ p.get_tipo_patrocinio_display }}{{ p.persona_contacto|default:"—" }}{{ p.email_contacto }}{{ p.fecha_contacto|date:"d/m/Y" }} -
-
- {% csrf_token %} - - -
- Editar -
-
No hay patrocinios rechazados.
-
-
-
- - {% endif %}{# end if temporada_actual #} -
-
- -{# ── Modal Proponer Patrocinio ── #} - - -{% endblock content %} diff --git a/Plataform_Web/temporadas/__init__.py b/Plataform_Web/temporadas/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/temporadas/admin.py b/Plataform_Web/temporadas/admin.py deleted file mode 100644 index 8420191..0000000 --- a/Plataform_Web/temporadas/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin -from .models import Temporada - -@admin.register(Temporada) -class TemporadaAdmin(admin.ModelAdmin): - # Columnas principales. - list_display = ('nombre', 'fecha_inicio', 'fecha_fin', 'presupuesto', 'actual') - - # Permite marcar o desmarcar el check de "actual" directamente desde la tabla, sin entrar al detalle - list_editable = ('actual',) - - # Filtro para filtrar por temporadas históricas o actual - list_filter = ('actual',) - - # Buscador de temporadas por nombre - search_fields = ('nombre',) - - # Filtro para ver los miembros de una temporada - filter_horizontal = ('miembros',) \ No newline at end of file diff --git a/Plataform_Web/temporadas/apps.py b/Plataform_Web/temporadas/apps.py deleted file mode 100644 index 1a7e5b5..0000000 --- a/Plataform_Web/temporadas/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class TemporadasConfig(AppConfig): - name = 'temporadas' diff --git a/Plataform_Web/temporadas/forms.py b/Plataform_Web/temporadas/forms.py deleted file mode 100644 index 4788a5d..0000000 --- a/Plataform_Web/temporadas/forms.py +++ /dev/null @@ -1,13 +0,0 @@ -from django import forms -from .models import Temporada - - -class TemporadaForm(forms.ModelForm): - class Meta: - model = Temporada - fields = ['nombre', 'fecha_inicio', 'fecha_fin', 'presupuesto', 'actual', 'miembros'] - widgets = { - 'fecha_inicio': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), - 'fecha_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), - 'miembros': forms.CheckboxSelectMultiple(), - } diff --git a/Plataform_Web/temporadas/migrations/0001_initial.py b/Plataform_Web/temporadas/migrations/0001_initial.py deleted file mode 100644 index ba154fb..0000000 --- a/Plataform_Web/temporadas/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 6.0.2 on 2026-02-17 11:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Temporada', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('nombre', models.CharField(max_length=50, unique=True)), - ('fecha_inicio', models.DateField()), - ('fecha_fin', models.DateField()), - ('presupuesto', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), - ('actual', models.BooleanField(default=False, verbose_name='¿Es la temporada actual?')), - ], - options={ - 'verbose_name': 'Temporada', - 'verbose_name_plural': 'Temporadas', - 'ordering': ['-fecha_inicio'], - }, - ), - ] diff --git a/Plataform_Web/temporadas/migrations/0002_temporada_miembros.py b/Plataform_Web/temporadas/migrations/0002_temporada_miembros.py deleted file mode 100644 index b32fb3d..0000000 --- a/Plataform_Web/temporadas/migrations/0002_temporada_miembros.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 6.0.2 on 2026-03-06 19:29 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('temporadas', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='temporada', - name='miembros', - field=models.ManyToManyField(blank=True, related_name='temporadas_participadas', to=settings.AUTH_USER_MODEL, verbose_name='Miembros del equipo'), - ), - ] diff --git a/Plataform_Web/temporadas/migrations/__init__.py b/Plataform_Web/temporadas/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/temporadas/models.py b/Plataform_Web/temporadas/models.py deleted file mode 100644 index 7d0e19c..0000000 --- a/Plataform_Web/temporadas/models.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.db import models -from django.conf import settings - -class Temporada(models.Model): - # El nombre será algo como "Gades 2024-25" - nombre = models.CharField(max_length=50, unique=True) - - fecha_inicio = models.DateField() - fecha_fin = models.DateField() - - # Presupuesto total para ese año - presupuesto = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) - - # Checkbox para marcar cuál es la temporada que estamos viviendo ahora - actual = models.BooleanField(default=False, verbose_name="¿Es la temporada actual?") - - # Relaciones - # Relacion N a N con miembros - miembros = models.ManyToManyField( - settings.AUTH_USER_MODEL, - related_name="temporadas_participadas", - blank=True, - verbose_name="Miembros del equipo" - ) - - class Meta: - verbose_name = "Temporada" - verbose_name_plural = "Temporadas" - ordering = ['-fecha_inicio'] # Ordena las más nuevas primero - - def __str__(self): - return self.nombre - - def save(self, *args, **kwargs): - # TRUCO PRO: Si marco esta temporada como "actual", desmarco todas las demás - # Así evitamos que haya dos temporadas activas a la vez. - if self.actual: - Temporada.objects.filter(actual=True).exclude(pk=self.pk).update(actual=False) - super().save(*args, **kwargs) \ No newline at end of file diff --git a/Plataform_Web/temporadas/tests.py b/Plataform_Web/temporadas/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/Plataform_Web/temporadas/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Plataform_Web/temporadas/views.py b/Plataform_Web/temporadas/views.py deleted file mode 100644 index 1db3713..0000000 --- a/Plataform_Web/temporadas/views.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib import messages -from django.views.decorators.http import require_POST -from users.decorators import require_rol -from .models import Temporada -from .forms import TemporadaForm - - -@require_rol('directiva') -def gestion_temporadas(request): - temporadas = Temporada.objects.all() - return render(request, 'gestion_temporadas.html', {'temporadas': temporadas}) - - -@require_rol('directiva') -def crear_temporada(request): - form = TemporadaForm(request.POST or None) - if request.method == 'POST' and form.is_valid(): - form.save() - messages.success(request, 'Temporada creada correctamente.') - return redirect('gestion_temporadas') - return render(request, 'editar_temporada.html', {'form': form, 'temporada': None}) - - -@require_rol('directiva') -def editar_temporada(request, pk): - temporada = get_object_or_404(Temporada, pk=pk) - form = TemporadaForm(request.POST or None, instance=temporada) - if request.method == 'POST' and form.is_valid(): - form.save() - messages.success(request, 'Temporada actualizada correctamente.') - return redirect('gestion_temporadas') - return render(request, 'editar_temporada.html', {'form': form, 'temporada': temporada}) - - -@require_rol('directiva') -@require_POST -def eliminar_temporada(request, pk): - temporada = get_object_or_404(Temporada, pk=pk) - temporada.delete() - messages.success(request, f'Temporada "{temporada.nombre}" eliminada.') - return redirect('gestion_temporadas') diff --git a/Plataform_Web/users/__init__.py b/Plataform_Web/users/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/users/admin.py b/Plataform_Web/users/admin.py deleted file mode 100644 index 81671c8..0000000 --- a/Plataform_Web/users/admin.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin -from .models import CustomUser - -@admin.register(CustomUser) -class CustomUserAdmin(UserAdmin): - list_display = ('username', 'first_name', 'last_name', 'rol', 'especialidad', 'is_staff') - - # Filtramos por rol y área técnica - list_filter = ('rol', 'especialidad', 'is_staff', 'is_active') - - # Buscador de usuarios - search_fields = ('username', 'first_name', 'last_name', 'email') - - # Para editar al usuario con los datos insertados - fieldsets = UserAdmin.fieldsets + ( - ('Información del Equipo Gades', { - 'fields': ('rol', 'especialidad'), - }), - ) - - # Para crear al usuario con los datos insertados - add_fieldsets = UserAdmin.add_fieldsets + ( - ('Información del Equipo Gades', { - 'fields': ('rol', 'especialidad'), - }), - ) \ No newline at end of file diff --git a/Plataform_Web/users/apps.py b/Plataform_Web/users/apps.py deleted file mode 100644 index 4ce1fab..0000000 --- a/Plataform_Web/users/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class UsersConfig(AppConfig): - name = 'users' diff --git a/Plataform_Web/users/decorators.py b/Plataform_Web/users/decorators.py deleted file mode 100644 index 28cd93a..0000000 --- a/Plataform_Web/users/decorators.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.contrib.auth.decorators import login_required, user_passes_test - - -def require_rol(*roles): - def decorator(view_func): - return login_required(user_passes_test(lambda u: u.rol in roles)(view_func)) - return decorator diff --git a/Plataform_Web/users/forms.py b/Plataform_Web/users/forms.py deleted file mode 100644 index 0d096c3..0000000 --- a/Plataform_Web/users/forms.py +++ /dev/null @@ -1,42 +0,0 @@ -from django import forms -from .models import CustomUser - - -class PerfilForm(forms.ModelForm): - class Meta: - model = CustomUser - fields = ['username', 'first_name', 'last_name', 'email'] - widgets = { - 'username': forms.TextInput(attrs={'class': 'form-control'}), - 'first_name': forms.TextInput(attrs={'class': 'form-control'}), - 'last_name': forms.TextInput(attrs={'class': 'form-control'}), - 'email': forms.EmailInput(attrs={'class': 'form-control'}), - } - labels = { - 'username': 'Usuario', - 'first_name': 'Nombre', - 'last_name': 'Apellidos', - 'email': 'Correo electrónico', - } - - -class EditarUsuarioForm(forms.ModelForm): - class Meta: - model = CustomUser - fields = ['username', 'first_name', 'last_name', 'email', 'rol', 'especialidad'] - widgets = { - 'username': forms.TextInput(attrs={'class': 'form-control'}), - 'first_name': forms.TextInput(attrs={'class': 'form-control'}), - 'last_name': forms.TextInput(attrs={'class': 'form-control'}), - 'email': forms.EmailInput(attrs={'class': 'form-control'}), - 'rol': forms.Select(attrs={'class': 'form-select'}), - 'especialidad': forms.Select(attrs={'class': 'form-select'}), - } - labels = { - 'username': 'Usuario', - 'first_name': 'Nombre', - 'last_name': 'Apellidos', - 'email': 'Correo electrónico', - 'rol': 'Rol en el equipo', - 'especialidad': 'Área técnica', - } diff --git a/Plataform_Web/users/migrations/0001_initial.py b/Plataform_Web/users/migrations/0001_initial.py deleted file mode 100644 index fedfc57..0000000 --- a/Plataform_Web/users/migrations/0001_initial.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 6.0.2 on 2026-02-17 10:36 - -import django.contrib.auth.models -import django.contrib.auth.validators -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='CustomUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('rol', models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('empleado', 'Empleado')], default='empleado', max_length=20)), - ('especialidad', models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/Plataform_Web/users/migrations/0002_alter_customuser_especialidad_alter_customuser_rol.py b/Plataform_Web/users/migrations/0002_alter_customuser_especialidad_alter_customuser_rol.py deleted file mode 100644 index 9c991e1..0000000 --- a/Plataform_Web/users/migrations/0002_alter_customuser_especialidad_alter_customuser_rol.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.2 on 2026-02-17 11:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='customuser', - name='especialidad', - field=models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True, verbose_name='Área Técnica'), - ), - migrations.AlterField( - model_name='customuser', - name='rol', - field=models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('empleado', 'Empleado')], default='empleado', max_length=20, verbose_name='Rol en el equipo'), - ), - ] diff --git a/Plataform_Web/users/migrations/0003_alter_customuser_especialidad_alter_customuser_rol.py b/Plataform_Web/users/migrations/0003_alter_customuser_especialidad_alter_customuser_rol.py deleted file mode 100644 index 68dc9a3..0000000 --- a/Plataform_Web/users/migrations/0003_alter_customuser_especialidad_alter_customuser_rol.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 6.0.2 on 2026-03-06 19:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0002_alter_customuser_especialidad_alter_customuser_rol'), - ] - - operations = [ - migrations.AlterField( - model_name='customuser', - name='especialidad', - field=models.CharField(blank=True, choices=[('aerodinamica', 'Aerodinámica'), ('chasis', 'Chasis'), ('business_operations', 'Business & Operations'), ('epowertrain', 'E-Powertrain'), ('electronica', 'Electrónica'), ('sdf', 'SDF'), ('motor_transmision', 'Motor & Transmisión'), ('software', 'Software')], max_length=30, null=True, verbose_name='Área Técnica'), - ), - migrations.AlterField( - model_name='customuser', - name='rol', - field=models.CharField(choices=[('directiva', 'Directiva'), ('jefe_area', 'Jefe de Área'), ('miembro', 'Miembro')], default='miembro', max_length=20, verbose_name='Rol en el equipo'), - ), - ] diff --git a/Plataform_Web/users/migrations/__init__.py b/Plataform_Web/users/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/Plataform_Web/users/models.py b/Plataform_Web/users/models.py deleted file mode 100644 index 2661b57..0000000 --- a/Plataform_Web/users/models.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib.auth.models import AbstractUser -from django.db import models - -class CustomUser(AbstractUser): - # --- 1. OPCIONES (El Menú) --- - ROL_CHOICES = ( - ('directiva', 'Directiva'), - ('jefe_area', 'Jefe de Área'), - ('miembro', 'Miembro'), - ) - - ESPECIALIDAD_CHOICES = ( - ('aerodinamica', 'Aerodinámica'), - ('chasis', 'Chasis'), - ('business_operations', 'Business & Operations'), - ('epowertrain', 'E-Powertrain'), - ('electronica', 'Electrónica'), - ('sdf', 'SDF'), - ('motor_transmision', 'Motor & Transmisión'), - ('software', 'Software'), - ) - - # --- 2. CAMPOS (Las Columnas en la BD) --- - # Usamos la versión con 'verbose_name' porque queda mejor en la web - rol = models.CharField( - max_length=20, - choices=ROL_CHOICES, - default='miembro', - verbose_name="Rol en el equipo" - ) - - especialidad = models.CharField( - max_length=30, - choices=ESPECIALIDAD_CHOICES, - null=True, - blank=True, - verbose_name="Área Técnica" - ) - - # --- 3. MÉTODOS --- - def __str__(self): - # Muestra: "Nombre Apellido (usuario) - Rol" - return f"{self.first_name} {self.last_name} ({self.username}) - {self.get_rol_display()}" \ No newline at end of file diff --git a/Plataform_Web/users/tests.py b/Plataform_Web/users/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/Plataform_Web/users/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/Plataform_Web/users/views.py b/Plataform_Web/users/views.py deleted file mode 100644 index 4a7fc52..0000000 --- a/Plataform_Web/users/views.py +++ /dev/null @@ -1,119 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.contrib.auth.forms import PasswordChangeForm -from django.contrib.auth import update_session_auth_hash -from django.contrib import messages -from django.views.decorators.http import require_POST -from .decorators import require_rol -from .forms import PerfilForm, EditarUsuarioForm -from .models import CustomUser - - -@login_required -def index(request): - return render(request, 'index.html') - - -@login_required -def mi_perfil(request): - perfil_form = PerfilForm(instance=request.user) - password_form = PasswordChangeForm(user=request.user) - - if request.method == 'POST': - if 'guardar_perfil' in request.POST: - perfil_form = PerfilForm(request.POST, instance=request.user) - if perfil_form.is_valid(): - perfil_form.save() - messages.success(request, 'Tus datos se han actualizado correctamente.') - return redirect('mi_perfil') - - elif 'cambiar_password' in request.POST: - password_form = PasswordChangeForm(user=request.user, data=request.POST) - if password_form.is_valid(): - user = password_form.save() - update_session_auth_hash(request, user) - messages.success(request, 'Tu contraseña se ha cambiado correctamente.') - return redirect('mi_perfil') - - context = { - 'perfil_form': perfil_form, - 'password_form': password_form, - } - return render(request, 'mi_perfil.html', context) - - -@login_required -def listado_miembros(request): - miembros = CustomUser.objects.all().order_by('last_name', 'first_name') - - rol = request.GET.get('rol', '') - especialidad = request.GET.get('especialidad', '') - apellido = request.GET.get('apellido', '') - - if rol: - miembros = miembros.filter(rol=rol) - if especialidad: - miembros = miembros.filter(especialidad=especialidad) - if apellido: - miembros = miembros.filter(last_name__icontains=apellido) - - context = { - 'miembros': miembros, - 'rol_choices': CustomUser.ROL_CHOICES, - 'especialidad_choices': CustomUser.ESPECIALIDAD_CHOICES, - 'filtro_rol': rol, - 'filtro_especialidad': especialidad, - 'filtro_apellido': apellido, - } - return render(request, 'listado_miembros.html', context) - - -@require_rol('directiva') -def gestion_usuarios(request): - usuarios = CustomUser.objects.all().order_by('last_name', 'first_name') - - rol = request.GET.get('rol', '') - especialidad = request.GET.get('especialidad', '') - apellido = request.GET.get('apellido', '') - - if rol: - usuarios = usuarios.filter(rol=rol) - if especialidad: - usuarios = usuarios.filter(especialidad=especialidad) - if apellido: - usuarios = usuarios.filter(last_name__icontains=apellido) - - context = { - 'usuarios': usuarios, - 'rol_choices': CustomUser.ROL_CHOICES, - 'especialidad_choices': CustomUser.ESPECIALIDAD_CHOICES, - 'filtro_rol': rol, - 'filtro_especialidad': especialidad, - 'filtro_apellido': apellido, - } - return render(request, 'gestion_usuarios.html', context) - - -@require_rol('directiva') -def editar_usuario(request, pk): - usuario = get_object_or_404(CustomUser, pk=pk) - - if request.method == 'POST': - form = EditarUsuarioForm(request.POST, instance=usuario) - if form.is_valid(): - form.save() - messages.success(request, 'El usuario se ha actualizado correctamente.') - return redirect('gestion_usuarios') - else: - form = EditarUsuarioForm(instance=usuario) - - return render(request, 'editar_usuario.html', {'form': form, 'usuario': usuario}) - - -@require_rol('directiva') -@require_POST -def eliminar_usuario(request, pk): - usuario = get_object_or_404(CustomUser, pk=pk) - usuario.delete() - messages.success(request, 'El usuario se ha eliminado correctamente.') - return redirect('gestion_usuarios') diff --git a/Firmware/G26-Telemetria/include/can.hpp b/include/can.hpp similarity index 93% rename from Firmware/G26-Telemetria/include/can.hpp rename to include/can.hpp index f790f73..7a03905 100644 --- a/Firmware/G26-Telemetria/include/can.hpp +++ b/include/can.hpp @@ -1,13 +1,13 @@ #ifndef CAN_HPP #define CAN_HPP -#define RX_PIN 23 -#define TX_PIN 22 +#define RX_PIN 13 +#define TX_PIN 38 #define POLLING_RATE_MS 1000 #define TRANSMIT_RATE_MS 1000 #include "driver/twai.h" -#include "common_libraries.hpp" +#include "common/common_libraries.hpp" #include "data_processor.hpp" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" diff --git a/include/common/common_libraries.hpp b/include/common/common_libraries.hpp new file mode 100644 index 0000000..f03e7c4 --- /dev/null +++ b/include/common/common_libraries.hpp @@ -0,0 +1,9 @@ +#ifndef COMMON_LIBRARIES_HPP +#define COMMON_LIBRARIES_HPP + +#include +#include "time.h" +#include +#include + +#endif \ No newline at end of file diff --git a/include/common/telemetry_status.hpp b/include/common/telemetry_status.hpp new file mode 100644 index 0000000..74e2971 --- /dev/null +++ b/include/common/telemetry_status.hpp @@ -0,0 +1,9 @@ +#ifndef TELEMETRY_STATUS_HPP +#define TELEMETRY_STATUS_HPP + +enum class TelemetryStatus { + CONNECTED, +}; + + +#endif \ No newline at end of file diff --git a/include/data_processor.hpp b/include/data_processor.hpp new file mode 100644 index 0000000..0523555 --- /dev/null +++ b/include/data_processor.hpp @@ -0,0 +1,38 @@ +#ifndef DATAPROCESSOR_HPP +#define DATAPROCESSOR_HPP + +#include "common/common_libraries.hpp" +#include "common/display_id.hpp" +#include "led_strip.hpp" +#include "crowpanel_controller.hpp" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +class DataProcessor { +public: + DataProcessor() = default; + char* process(std::vector data); + void send_serial(byte type, unsigned int value); + void send_serial_frame_0(int rpmh, int rpml, int tpsh, int tpsl, int ecth, int ectl, int gear); + void send_serial_frame_1(int lfws, int rfws, int lrws, int rrws, int maph, int mapl, int ect); + void send_serial_frame_2(int lambh, int lambl, int lamth, int lamtl, int bvolth, int bvoltl, int iat); + void send_serial_frame_3(int aux1, int aux2, int aux3, int aux4, int aux5, int aux6, int aux7); + void send_serial_frame_4(int aux1, int aux2, int aux3, int aux4, int aux5, int aux6, int aux7); + void send_serial_change_display(int display); + void send_serial_screen_test(int test); + void set_led_strip(LedStrip *led_strip){ + _led_strip = led_strip; + } + void set_crow_panel_controller(CrowPanelController *crow_panel_controller) { + _crow_panel_controller = crow_panel_controller; + } + +private: + LedStrip *_led_strip; + CrowPanelController *_crow_panel_controller; + int current_display=0; + bool change_screen_requested=false; +}; + +#endif \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..886c04d --- /dev/null +++ b/index.html @@ -0,0 +1,254 @@ + + + + + Telemetría IoT - Sesiones + + + + +
+

🔬 Telemetría IoT

+
❌ Sesión Inactiva
+
--- kg
+
Socket: desconocido
+ +
+
Ventanas recibidas
0
+
Muestras totales
0
+
Última actualización
---
+
+ +
+ + + +
+ +
+
+ + + + + + + diff --git a/Firmware/G26-Telemetria/src/can.cpp b/src/can.cpp similarity index 79% rename from Firmware/G26-Telemetria/src/can.cpp rename to src/can.cpp index 4bf860e..f4ef323 100644 --- a/Firmware/G26-Telemetria/src/can.cpp +++ b/src/can.cpp @@ -1,22 +1,17 @@ /** * @file can.cpp - * @author Raul Arcos Herrera + * @author Raúl Arcos Herrera * @brief This file contains the implementation of the CAN Controller class for Link G4+ ECU. */ #include "../include/can.hpp" -// Volcado en crudo de TODOS los mensajes del bus, a la velocidad a la que -// llegan. Satura los 115200 baudios y frena la tarea de escucha, asi que solo -// debe activarse para depurar el bus. -#define VOLCADO_CRUDO_CAN 0 - static bool driver_installed = false; void CAN::start() { Serial.println("Starting CAN Controller..."); twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)TX_PIN, (gpio_num_t)RX_PIN, TWAI_MODE_NORMAL); - twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); + twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS(); twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); esp_err_t install_status = twai_driver_install(&g_config, &t_config, &f_config); @@ -46,6 +41,7 @@ void CAN::start() { return; } + // TWAI driver is now successfully installed and started driver_installed = true; } @@ -67,10 +63,20 @@ void CAN::start_listening_task() { "CAN_Listen_Task", // Task name 4096, // Stack size (words) this, // Task parameter (this CAN instance) - 1, // Priority + 1, // Priority (lowered from 5 to 1) &_listen_task_handle // Task handle ); + // BaseType_t result = xTaskCreatePinnedToCore( + // listenTask, // Task function + // "CAN_Listen_Task", // Task name + // 4096, // Stack size (words) + // this, // Task parameter (this CAN instance) + // 1, // Priority + // &_listen_task_handle, // Task handle + // 0 // Core 0 (main loop typically runs on Core 1) + // ); + if (result == pdPASS) { Serial.println("CAN listening task created successfully"); } else { @@ -86,6 +92,7 @@ void CAN::stop_listening_task() { if (_listen_task_handle != NULL) { _should_stop_listening = true; + // Wait for task to finish (max 1 second) for (int i = 0; i < 100; i++) { if (_listen_task_handle == NULL) { break; @@ -93,6 +100,7 @@ void CAN::stop_listening_task() { vTaskDelay(pdMS_TO_TICKS(10)); } + // Force delete if still running if (_listen_task_handle != NULL) { vTaskDelete(_listen_task_handle); _listen_task_handle = NULL; @@ -124,17 +132,21 @@ twai_message_t CAN::createBoolMessage(bool b0, bool b1, bool b2, bool b3, bool b void CAN::listen() { Serial.println("CAN listening task started"); + // Continuous loop for the thread while (!_should_stop_listening) { if (!driver_installed) { + // Driver not installed vTaskDelay(pdMS_TO_TICKS(1000)); continue; } + // Check if alert happened uint32_t alerts_triggered; - twai_read_alerts(&alerts_triggered, 0); + twai_read_alerts(&alerts_triggered, pdMS_TO_TICKS(1000)); // Reduced timeout for more responsiveness twai_status_info_t twaistatus; twai_get_status_info(&twaistatus); + // Handle alerts if (alerts_triggered & TWAI_ALERT_ERR_PASS) { Serial.println("Alert: TWAI controller has become error passive."); } @@ -154,7 +166,7 @@ void CAN::listen() { int message_count = 0; while (twai_receive(&message, 0) == ESP_OK && !_should_stop_listening) { bool all_zeros = true; - for (int i = 1; i < message.data_length_code; i++) { + for (int i = 0; i < message.data_length_code; i++) { if (message.data[i] != 0) { all_zeros = false; break; @@ -162,26 +174,24 @@ void CAN::listen() { } if (all_zeros) { -#if VOLCADO_CRUDO_CAN Serial.println("Ignoring message with all zero data"); -#endif taskYIELD(); continue; } - -#if VOLCADO_CRUDO_CAN + if (message.extd) { Serial.println("Extended Format"); } else { Serial.println("Standard Format"); } Serial.printf("ID: %lx\nByte:", message.identifier); - for (int i = 0; i < message.data_length_code && !(message.rtr); i++) { - Serial.printf(" %d = %02x,", i, message.data[i]); - } - Serial.println(""); -#endif if (!(message.rtr)) { + for (int i = 0; i < message.data_length_code; i++) { + Serial.printf(" %d = %02x,", i, message.data[i]); + } + Serial.println(""); + + // Send to data processor based on first byte (maintaining original logic) switch (message.data[0]) { case 0: _data_processor->send_serial_frame_0(message.data[1], message.data[2], message.data[3], message.data[4], message.data[5], message.data[6], message.data[7]); @@ -190,14 +200,10 @@ void CAN::listen() { _data_processor->send_serial_frame_1(message.data[1], message.data[2], message.data[3], message.data[4], message.data[5], message.data[6], message.data[7]); break; case 2: - _data_processor->send_serial_frame_2(message.data[1], message.data[2], message.data[3], message.data[4], message.data[5], message.data[6], message.data[7]); - break; - case 3: + _data_processor->send_serial_frame_2(message.data[1], message.data[2], message.data[3], message.data[4], message.data[5], message.data[6], message.data[7]); + break; + case 3: _data_processor->send_serial_frame_3(message.data[1], message.data[2], message.data[3], message.data[4], message.data[5], message.data[6], message.data[7]); - break; - case 4: - _data_processor->send_serial_frame_4(message.data[1], message.data[2], message.data[3], message.data[4], message.data[5], message.data[6], message.data[7]); - break; default: break; } @@ -213,5 +219,5 @@ void CAN::listen() { Serial.println("CAN listening task ending"); _listen_task_handle = NULL; - vTaskDelete(NULL); + vTaskDelete(NULL); // Delete this task } diff --git a/src/data_processor.cpp b/src/data_processor.cpp new file mode 100644 index 0000000..fcf6e74 --- /dev/null +++ b/src/data_processor.cpp @@ -0,0 +1,290 @@ +/** + * @file data_processor.cpp + * @author Raúl Arcos Herrera + * @brief This file contains the implementation of the Data Processor class for Link G4+ ECU. + */ + +#include "../include/data_processor.hpp" + +void DataProcessor::send_serial(byte type, unsigned int value) { //Como parámetros se pasan el ID (type), que es el ID establecido al inicio del código para el dato que se quiera enviar. Ej: RPM_ID -> 0x51; y se envía el valor de dicho dato. + byte dato[8] = { 0x5A, 0xA5, 0x05, 0x82, 0x00, 0x00, 0x00, 0x00 }; //Se establece un arreglo de bytes con los primeros datos necesarios para que la pantalla lo interprete como mensaje (En la Wiki hay tutoriales que lo explican a fondo), como ser la longitud y el tipo de mensaje. + dato[4] = type; //Se configura en el mensaje el ID correspondiente al dato a enviar. + dato[6] = (value >> 8) & 0xFF; //Se configura el dato en los últimos 2 bytes. + dato[7] = value & 0xFF; + + Serial.write(dato, 8); //Se envía serialmente el mensaje, indicando su longituden bytes para ello. +} + +//RPM + TPS + vBatt + ECT +void DataProcessor::send_serial_frame_0(int rpmh, int rpml, int tpsh, int tpsl, int vbatth, int vbattl, int ect){ + Serial.println("send_serial_frame_0"); + + int rpm = (rpmh * 256) + rpml; + int tps = (tpsh * 256) + tpsl; + double vbatt = ((vbatth * 256) + vbattl) / 100.0; + + _crow_panel_controller->set_value_to_label(ui_rpm, rpm); + _crow_panel_controller->set_value_to_label(ui_battvolt, vbatt); + _crow_panel_controller->set_value_to_label(ui_ect, ect); + _crow_panel_controller->set_value_to_label(ui_ect2, ect); + + // Update RPM LED bar (8000-12500 RPM range) + _crow_panel_controller->update_rpm_bar(rpm); + + // Battery voltage color (typical car battery: 12.6V resting, 13.2-14.4V running) + if (vbatt < 11.5) { + _crow_panel_controller->set_label_color(ui_battvolt, CrowPanelController::COLOR_CRITICAL); // Red for low + } else if (vbatt < 12.0) { + _crow_panel_controller->set_label_color(ui_battvolt, CrowPanelController::COLOR_WARNING); // Yellow for warning + } else if (vbatt > 15.0) { + _crow_panel_controller->set_label_color(ui_battvolt, CrowPanelController::COLOR_WARNING); // Yellow for overcharge + } else { + _crow_panel_controller->set_label_color(ui_battvolt, CrowPanelController::COLOR_NORMAL); // Green for good + } + + //El numero que muestra la temperatura siempre será blanco + _crow_panel_controller->set_label_color(ui_ect, CrowPanelController::COLOR_PANEL_DEFAULT); + _crow_panel_controller->set_label_color(ui_ect2, CrowPanelController::COLOR_PANEL_DEFAULT); + + // Engine coolant temperature (typical range: 80-105°C normal operating temp) + if (ect > 105) { + // Crítico: Rojo + _crow_panel_controller->set_panel_color(ui_PanelETC, CrowPanelController::COLOR_CRITICAL); + } else if (ect >= 95) { + // Advertencia: Amarillo (95 a 105) + _crow_panel_controller->set_panel_color(ui_PanelETC, CrowPanelController::COLOR_WARNING); + } else if (ect >= 65) { + //Temperatura Ideal: Verde (65 a 94) + _crow_panel_controller->set_panel_color(ui_PanelETC, CrowPanelController::COLOR_GOOD); + } else { // etc <= 60 Azul + _crow_panel_controller->set_panel_color(ui_PanelETC, CrowPanelController::COLOR_BLUE); + } +} + +//LAMB + LAMBTRG + FUEL + GEAR +void DataProcessor::send_serial_frame_1(int lmbh, int lmbl, int lmbth, int lmbtl, int fuelh, int fuell, int gear){ + Serial.println("send_serial_frame_1"); + int lmb = (lmbh * 256) + lmbl; + int lmbtrg = (lmbth * 256) + lmbtl; + int fuel = (fuelh * 256) + fuell; + _crow_panel_controller->set_value_to_label(ui_lambda, lmb); + _crow_panel_controller->set_value_to_label(ui_lambdatarget, lmbtrg); + _crow_panel_controller->set_value_to_label(ui_fuel, fuel); +// _crow_panel_controller->set_value_to_label(ui_gear, gear); +} + + +void DataProcessor::send_serial_frame_2(int shut, int fan, int lmbch, int lmbcl, int brakeh, int brakel, int aux1){ + Serial.println("send_serial_frame_2"); + int lmbcorrect = (lmbch * 256) + lmbcl; + int brake = (brakeh * 256) + brakel; + + char shut_str[10]; + char fan_str[10]; + char aux1_str[10]; + + if (shut == 3){ + strcpy(shut_str, "ON"); + } else { + strcpy(shut_str, "OFF"); + } + + if (fan == 1){ + strcpy(fan_str, "ON"); + } else { + strcpy(fan_str, "OFF"); + } + + if (aux1 == 1){ + strcpy(aux1_str, "N"); + _crow_panel_controller->set_label_color(ui_PanelGear, CrowPanelController::COLOR_GOOD); + } else { + strcpy(aux1_str, "D"); + _crow_panel_controller->set_label_color(ui_PanelGear, CrowPanelController::COLOR_PANEL_DEFAULT); + } + + + + _crow_panel_controller->set_string_to_label(ui_shutdown, shut_str); + _crow_panel_controller->set_string_to_label(ui_fan, fan_str); + _crow_panel_controller->set_value_to_label(ui_correctionlambda, lmbcorrect); + _crow_panel_controller->set_value_to_label(ui_auxstatus9, brake); + _crow_panel_controller->set_string_to_label(ui_gear, aux1_str); + + // Shutdown status color + if (shut == 3) { + _crow_panel_controller->set_label_color(ui_shutdown, CrowPanelController::COLOR_CRITICAL); // Red when shutdown is ON (emergency) + } else { + _crow_panel_controller->set_label_color(ui_shutdown, CrowPanelController::COLOR_GOOD); // Green when shutdown is OFF (normal) + } + + // Fan status color + if (fan == 1) { + _crow_panel_controller->set_label_color(ui_fan, CrowPanelController::COLOR_BLUE); // Blue when fan is ON (cooling) + } else { + _crow_panel_controller->set_label_color(ui_fan, CrowPanelController::COLOR_NORMAL); // White when fan is OFF + } + + // Brake pressure color (assuming brake > 0 means brakes applied) + if (brake > 100) { // Adjust threshold as needed + _crow_panel_controller->set_label_color(ui_auxstatus9, CrowPanelController::COLOR_WARNING); // Yellow for heavy braking + } else if (brake > 0) { + _crow_panel_controller->set_label_color(ui_auxstatus9, CrowPanelController::COLOR_NORMAL); // White for light braking + } else { + _crow_panel_controller->set_label_color(ui_auxstatus9, CrowPanelController::COLOR_GOOD); // Green for no braking + } +} + +void DataProcessor::send_serial_frame_3(int aux3, int aux4, int aux5, int aux6, int aux7, int aux8, int dig1){ + Serial.println("send_serial_frame_3"); + + char aux3_str[10]; + char aux4_str[10]; + char aux5_str[10]; + char aux6_str[10]; + char aux7_str[10]; + char aux8_str[10]; + char dig1_str[10]; + + if (aux3 == 1){ + strcpy(aux3_str, "ON"); + } else { + strcpy(aux3_str, "OFF"); + } + + if (aux4 == 1){ + + strcpy(aux4_str, "ON"); + } else { + strcpy(aux4_str, "OFF"); + } + + if (aux5 == 1){ + strcpy(aux5_str, "ON"); + } else { + strcpy(aux5_str, "OFF"); + } + + if (aux6 == 1){ + strcpy(aux6_str, "ON"); + } else { + strcpy(aux6_str, "OFF"); + } + + if (aux7 == 1){ + strcpy(aux7_str, "ON"); + } else { + strcpy(aux7_str, "OFF"); + } + + if (aux8 == 1){ + strcpy(aux8_str, "ON"); + } else { + strcpy(aux8_str, "OFF"); + } + + if (dig1 == 1){ + strcpy(dig1_str, "ON"); + } else { + strcpy(dig1_str, "OFF"); + } + + _crow_panel_controller -> set_string_to_label(ui_auxstatus3, aux3_str); + if(aux3 == 1 && change_screen_requested == false){ + switch(current_display){ + case 0: + _crow_panel_controller->change_screen(ui_Screen1); + break; + case 1: + _crow_panel_controller->change_screen(ui_Screen2); + break; + case 2: + _crow_panel_controller->change_screen(ui_Screen3); + break; + case 3: + _crow_panel_controller->change_screen(ui_Screen4); + break; + } + current_display++; + change_screen_requested = true; + if(current_display > 3){ + current_display = 0; + } + }else if(aux3 == 0 && change_screen_requested == true){ + change_screen_requested = false; + } + + _crow_panel_controller -> set_string_to_label(ui_auxstatus4, aux4_str); + _crow_panel_controller -> set_string_to_label(ui_auxstatus5, aux5_str); + _crow_panel_controller -> set_string_to_label(ui_auxstatus6, aux6_str); + _crow_panel_controller -> set_string_to_label(ui_auxstatus7, aux7_str); + _crow_panel_controller -> set_string_to_label(ui_auxstatus8, aux8_str); + _crow_panel_controller -> set_string_to_label(ui_digitalstatus1, dig1_str); +} + +void DataProcessor::send_serial_frame_4(int dig3, int dig4, int dig5, int dig6, int dig7, int dig8, int dig9){ + Serial.println("send_serial_frame_4"); + + char dig3_str[10]; + char dig4_str[10]; + char dig5_str[10]; + char dig6_str[10]; + char dig7_str[10]; + char dig8_str[10]; + char dig9_str[10]; + + if (dig3 == 1){ + strcpy(dig3_str, "ON"); + } else { + strcpy(dig3_str, "OFF"); + } + + if (dig4 == 1){ + strcpy(dig4_str, "ON"); + } else { + strcpy(dig4_str, "OFF"); + } + + if (dig5 == 1){ + strcpy(dig5_str, "ON"); + } else { + strcpy(dig5_str, "OFF"); + } + + if (dig6 == 1){ + strcpy(dig6_str, "ON"); + } else { + strcpy(dig6_str, "OFF"); + } + + if (dig7 == 1){ + strcpy(dig7_str, "ON"); + } else { + strcpy(dig7_str, "OFF"); + } + + if (dig8 == 1){ + strcpy(dig8_str, "ON"); + } else { + strcpy(dig8_str, "OFF"); + } + + if (dig9 == 1){ + strcpy(dig9_str, "ON"); + } else { + strcpy(dig9_str, "OFF"); + } + + _crow_panel_controller -> set_string_to_label(ui_digitalstatus3, dig3_str); + _crow_panel_controller -> set_string_to_label(ui_digitalstatus4, dig4_str); + _crow_panel_controller -> set_string_to_label(ui_digitalstatus5, dig5_str); + _crow_panel_controller -> set_string_to_label(ui_digitalstatus6, dig6_str); + _crow_panel_controller -> set_string_to_label(ui_digitalstatus7, dig7_str); + _crow_panel_controller -> set_string_to_label(ui_digitalstatus8, dig8_str); + _crow_panel_controller -> set_string_to_label(ui_digitalstatus9, dig9_str); +} + +void DataProcessor::send_serial_screen_test(int test) { + _crow_panel_controller->set_value_to_label(ui_rpm, test); + Serial.println(test); +} \ No newline at end of file