Actualización visual de la telemetria con velocidad y grafica acelerador freno

This commit is contained in:
adrigongv23 2026-07-27 13:34:00 +02:00
parent d3f2d1596e
commit 7c069efa14
2 changed files with 108 additions and 59 deletions

View file

@ -28,11 +28,11 @@ while True:
# Régimen siguiendo al acelerador, con algo de inercia # Régimen siguiendo al acelerador, con algo de inercia
rpm = 1200 + 12800 * (carga ** 0.7) + random.uniform(-120, 120) rpm = 1200 + 12800 * (carga ** 0.7) + random.uniform(-120, 120)
# Marcha: sube con el régimen, 0 = punto muerto # Velocidad: correlada con el régimen (modelo simple para el simulador)
if rpm < 1500: velocidad = 15 + 105 * (carga ** 0.8) + random.uniform(-1.5, 1.5)
marcha = 0
else: # Freno delantero: presión (bar). Frena al levantar el pie del acelerador
marcha = min(6, 1 + int((rpm - 1500) / 2300)) 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 # 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) ect = 88 + 6 * math.sin(t / 20.0) + 4 * carga + random.uniform(-0.3, 0.3)
@ -66,7 +66,8 @@ while True:
'lambda': f'{lambda_val:.3f}', 'lambda': f'{lambda_val:.3f}',
'lambda_obj': f'{lambda_obj:.3f}', 'lambda_obj': f'{lambda_obj:.3f}',
'tps': f'{tps:.0f}', 'tps': f'{tps:.0f}',
'marcha': f'{marcha}', 'velocidad': f'{velocidad:.0f}',
'freno_del': f'{freno_del:.1f}',
} }
mensaje = ';'.join(f'{clave}={valor}' for clave, valor in campos.items()) mensaje = ';'.join(f'{clave}={valor}' for clave, valor in campos.items())
else: else:

View file

@ -4,7 +4,7 @@ import csv
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.animation as animation import matplotlib.animation as animation
from matplotlib.gridspec import GridSpec from matplotlib.gridspec import GridSpec
from matplotlib.patches import Rectangle from matplotlib.patches import Rectangle, Polygon
from matplotlib.lines import Line2D from matplotlib.lines import Line2D
from matplotlib.colors import to_rgb from matplotlib.colors import to_rgb
from collections import deque from collections import deque
@ -28,8 +28,8 @@ MAX_PUNTOS = FRECUENCIA_HZ * VENTANA_SEG
# 1. "87.3|9200|14.2" -> ECT | RPM | BATERÍA # 1. "87.3|9200|14.2" -> ECT | RPM | BATERÍA
# #
# 2. Nuevo (clave-valor, para cuando el firmware envíe el resto de canales): # 2. Nuevo (clave-valor, para cuando el firmware envíe el resto de canales):
# "ect=87.3;rpm=9200;vbatt=14.2;pcomb=3.6;taceite=104;paceite=4.2; # "ect=87.3;rpm=9200;vbatt=14.2;velocidad=64;tps=45;freno_del=8.5;
# map=98;lambda=0.88;lambda_obj=0.88;tps=45;marcha=3" # pcomb=3.6;taceite=104;paceite=4.2;map=98;lambda=0.88;lambda_obj=0.88"
CLAVES_LEGADO = ('ect', 'rpm', 'vbatt') CLAVES_LEGADO = ('ect', 'rpm', 'vbatt')
# --- PALETA (tomada del logo oficial del equipo) --- # --- PALETA (tomada del logo oficial del equipo) ---
@ -49,8 +49,12 @@ APAGADO = '#18213A' # Segmento / relleno inactivo
MAX_RPM = 13000 # Fondo de escala del indicador MAX_RPM = 13000 # Fondo de escala del indicador
RPM_CORTE = 12000 # Zona roja / corte de inyección RPM_CORTE = 12000 # Zona roja / corte de inyección
# --- MARCHAS --- # --- PEDALES ---
VALOR_NEUTRAL = 0 # Valor con el que la ECU codifica el punto muerto 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 --- # --- DEFINICIÓN DE CANALES ---
# Cada canal declara su rango visible, sus zonas de color y una referencia # Cada canal declara su rango visible, sus zonas de color y una referencia
@ -91,6 +95,21 @@ CANALES = {
etiqueta='TPS', descripcion='ACELERADOR', unidad='%', etiqueta='TPS', descripcion='ACELERADOR', unidad='%',
vmin=0, vmax=100, decimales=0, referencia=None, vmin=0, vmax=100, decimales=0, referencia=None,
zonas=[(float('inf'), AZUL)]), 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( 'rpm': dict(
etiqueta='RPM', descripcion='RÉGIMEN DE MOTOR', unidad='', etiqueta='RPM', descripcion='RÉGIMEN DE MOTOR', unidad='',
vmin=0, vmax=MAX_RPM, decimales=0, referencia=None, vmin=0, vmax=MAX_RPM, decimales=0, referencia=None,
@ -141,7 +160,8 @@ aviso_pie = ''
aviso_hasta = 0.0 aviso_hasta = 0.0
# Orden de las columnas del CSV de sesión # Orden de las columnas del CSV de sesión
COLUMNAS_CSV = ['ect', 'rpm', 'vbatt', 'pcomb', 'taceite', 'paceite', 'map', 'lambda', 'tps', 'marcha'] 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 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT)) sock.bind((UDP_IP, UDP_PORT))
@ -218,7 +238,7 @@ def formatear(clave, valor):
def valor_csv(clave): def valor_csv(clave):
"""Valor para el fichero de sesión: sin separador de millares, que rompería """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 marcha son enteros).""" el CSV, y con los decimales propios del canal (RPM y velocidad son enteros)."""
valor = ultima_lectura.get(clave) valor = ultima_lectura.get(clave)
if valor is None: if valor is None:
return '' return ''
@ -226,13 +246,6 @@ def valor_csv(clave):
return f'{valor:.{decimales}f}' return f'{valor:.{decimales}f}'
def formatear_marcha(valor):
if valor is None:
return '-'
valor = int(round(valor))
return 'N' if valor == VALOR_NEUTRAL else str(valor)
def formato_tiempo(segundos): def formato_tiempo(segundos):
if segundos is None: if segundos is None:
return '--:--' return '--:--'
@ -349,8 +362,8 @@ fig.text(0.50, 0.017, 'R grabar S captura 1-7 gráfica F
fontsize=8.5, color=TXT_DIM, ha='center', va='center') 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') 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 + MARCHA + ACELERADOR) --- # --- BANDA A: LO QUE HACE EL PILOTO (RPM + VELOCIDAD + PEDALES) ---
gs_a = GridSpec(1, 3, width_ratios=[3.0, 0.72, 0.85], 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) left=0.035, right=0.975, top=0.893, bottom=0.712, wspace=0.030)
# A1. Luces de cambio # A1. Luces de cambio
@ -385,26 +398,58 @@ 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, txt_rpm = ax_rpm.text(98, 0.55, '--', fontsize=30, fontweight='bold', color=TXT_DIM,
ha='right', va='center') ha='right', va='center')
# A2. Marcha: el dato más glanceable de un panel, por eso va a tamaño máximo # A2. Velocidad: número grande, hereda el hueco glanceable que dejó la marcha
ax_marcha = fig.add_subplot(gs_a[0, 1]) ax_vel = fig.add_subplot(gs_a[0, 1])
preparar_panel(ax_marcha, AZUL) preparar_panel(ax_vel, AZUL)
ax_marcha.text(0.5, 0.88, 'MARCHA', fontsize=9, color=TXT_DIM, ha='center', va='center') ax_vel.text(0.5, 0.86, 'VELOCIDAD', fontsize=9, color=TXT_DIM, ha='center', va='center')
txt_marcha = ax_marcha.text(0.5, 0.42, '-', fontsize=62, fontweight='bold', color=TXT_DIM, txt_vel = ax_vel.text(0.5, 0.45, '--', fontsize=54, fontweight='bold', color=TXT_DIM,
ha='center', va='center') ha='center', va='center')
ax_vel.text(0.5, 0.13, 'km/h', fontsize=12, color=TXT_DIM, ha='center', va='center')
# A3. Acelerador # A3. Traza de pedales: acelerador (verde) y freno (rojo) oscilando de 0 a 100 %,
ax_tps = fig.add_subplot(gs_a[0, 2]) # como en la telemetría real. El freno es presión (bar) normalizada a % en pantalla.
preparar_panel(ax_tps, AZUL) ax_ped = fig.add_subplot(gs_a[0, 2])
ax_tps.text(0.09, 0.86, 'TPS', fontsize=9, color=TXT_DIM, va='center') ax_ped.set_facecolor(PANEL)
txt_tps = ax_tps.text(0.93, 0.84, '--', fontsize=20, fontweight='bold', color=TXT_DIM, for lado in ('top', 'right'):
ha='right', va='center') ax_ped.spines[lado].set_visible(False)
X_TPS, ANCHO_TPS, Y_TPS, ALTO_TPS = 0.09, 0.84, 0.30, 0.26 for lado in ('bottom', 'left'):
ax_tps.add_patch(Rectangle((X_TPS, Y_TPS), ANCHO_TPS, ALTO_TPS, facecolor=APAGADO)) ax_ped.spines[lado].set_color(GRID)
bar_tps = Rectangle((X_TPS, Y_TPS), 0, ALTO_TPS, facecolor=AZUL) ax_ped.set_xlim(0, PUNTOS_PEDALES)
ax_tps.add_patch(bar_tps) ax_ped.set_ylim(0, 105)
for pct in (0, 50, 100): ax_ped.set_yticks([0, 50, 100])
x = X_TPS + ANCHO_TPS * pct / 100 ax_ped.set_yticklabels(['0', '50', '100'], fontsize=7)
ax_tps.text(x, 0.22, f'{pct}', fontsize=7, color=TXT_DIM, ha='center', va='top') 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) --- # --- BANDA B: CANAL CON FOCO (número grande + gráfica) ---
gs_b = GridSpec(1, 2, width_ratios=[1, 3.3], gs_b = GridSpec(1, 2, width_ratios=[1, 3.3],
@ -704,7 +749,7 @@ def update(frame):
txt_hz.set_text('-- Hz') txt_hz.set_text('-- Hz')
txt_hz.set_color(TXT_DIM) txt_hz.set_color(TXT_DIM)
# --- BANDA A: RPM, MARCHA, TPS --- # --- BANDA A: RPM, VELOCIDAD, PEDALES ---
valor_rpm = ultima_lectura.get('rpm') if conectado else None valor_rpm = ultima_lectura.get('rpm') if conectado else None
if valor_rpm is not None: if valor_rpm is not None:
valor_rpm = max(0, min(MAX_RPM, valor_rpm)) valor_rpm = max(0, min(MAX_RPM, valor_rpm))
@ -726,25 +771,28 @@ def update(frame):
txt_rpm.set_text('--') txt_rpm.set_text('--')
txt_rpm.set_color(TXT_DIM) txt_rpm.set_color(TXT_DIM)
valor_marcha = ultima_lectura.get('marcha') if conectado else None # Velocidad: número grande
txt_marcha.set_text(formatear_marcha(valor_marcha)) valor_vel = ultima_lectura.get('velocidad') if conectado else None
if valor_marcha is None: txt_vel.set_text(formatear('velocidad', valor_vel))
txt_marcha.set_color(TXT_DIM) txt_vel.set_color(TXT if valor_vel is not None else TXT_DIM)
elif int(round(valor_marcha)) == VALOR_NEUTRAL:
txt_marcha.set_color(VERDE)
else:
txt_marcha.set_color(TXT)
valor_tps = ultima_lectura.get('tps') if conectado else None # Traza de pedales: acelerador (0-100 %) y freno (presión -> % de FRENO_PRESION_MAX)
if valor_tps is not None: serie_tps = np.array(historial['tps'], dtype=float)[-PUNTOS_PEDALES:]
fraccion = min(max(valor_tps / 100.0, 0.0), 1.0) serie_freno = np.array(historial['freno_del'], dtype=float)[-PUNTOS_PEDALES:]
bar_tps.set_width(ANCHO_TPS * fraccion) freno_pct = np.clip(serie_freno / FRENO_PRESION_MAX * 100.0, 0, 100)
txt_tps.set_text(f'{formatear("tps", valor_tps)} %') xs_ped = np.arange(len(serie_tps), dtype=float)
txt_tps.set_color(TXT) 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: else:
bar_tps.set_width(0) txt_ped_freno.set_text('FRENO --')
txt_tps.set_text('--')
txt_tps.set_color(TXT_DIM)
# --- BANDA C: TARJETAS --- # --- BANDA C: TARJETAS ---
alarmas = [] alarmas = []