Compare commits

..

No commits in common. "4c5967d3c5fe5cedd0c0ca9abaad6eb64a04ce31" and "100d19762e5c3204c1a34bfbe98aec1c88e5de56" have entirely different histories.

33 changed files with 966 additions and 6965 deletions

View file

@ -10,7 +10,7 @@ El sistema fue diseñado y calibrado a medida para el motor **Ford Ka Endura-E 1
### 1. Cuadrante de 4 Instrumentos Simultáneos
* **Temperatura de Refrigerante (PID 0105):** Rango 60°C a 120°C. Color dinámico (verde en rango normal, amarillo de advertencia y rojo a partir de 110°C con alarma sonora y visual).
* **Velocidad del Vehículo (Cálculo GPS Satelital + Fallback OBD-II):** Velocidad real de avance medida por satélite con precisión milimétrica (`gps.speed.kmph()`). Muestra el distintivo `[ GPS ]` en el cuadrante. Si se pierde señal (túnel o estacionamiento subterráneo), conmuta de forma transparente al sensor del vehículo (PID 010D).
* **Velocidad del Vehículo (PID 010D):** Rango 0 a 160 km/h.
* **Revoluciones por Minuto (PID 010C):** Rango 0 a 6.000 RPM.
* **Voltaje de Batería (Comando ATRV):** Rango 10.0V a 16.0V en tiempo real directo del borne del ELM327.
@ -27,32 +27,7 @@ El sistema fue diseñado y calibrado a medida para el motor **Ford Ka Endura-E 1
* **Diseño táctico:** Pastilla rectangular con bordes redondeados en verde esmeralda brillante (`[ ECO ]`) que se ilumina en el cuadrante de RPM y en el encabezado de pantalla completa.
* Al salir del rango óptimo, se apaga de inmediato restaurando de forma invisible el degradado del fondo.
### 4. Pantalla Tri-Temperatura (Página 2)
* Tres tarjetas tácticas con barra de progreso y lectura numérica digital:
* **Refrigerante:** Lectura en vivo de ECU OBD-II.
* **Habitáculo (Interior):** Sensor digital Dallas DS18B20 #1 en bus 1-Wire (GPIO 22).
* **Ambiente (Exterior):** Sensor digital Dallas DS18B20 #2 en bus 1-Wire (GPIO 22).
### 5. Pantalla Reloj Digital GPS & Fecha (Página 3)
* **Hora Satelital Atómica:** Dígitos gigantes en tipografía **DSEG7 Modern 80pt** perfectamente centrados en la tarjeta superior, sincronizados con satélites GPS en huso horario UTC-3 (Argentina).
* **Dos Puntos Parpadeantes:** Pulso rítmico continuo entre horas y minutos.
* **Badge Calendario y Fecha:** Estructurado en dos bloques:
* **Badge Calendario (Izquierda):** Cabecera en color de tema con el mes abreviado a 3 letras (`SEP`, `OCT`, `NOV`...) y número del día en 34pt 7 segmentos.
* **Información Textual (Derecha):** Día de la semana completo en mayúsculas (`VIERNES`), línea divisoria bitono y año (`2026`).
### 6. Pantalla de Telemetría GPS y Navegación (Página 4)
* **Altitud (msnm):** Elevación respecto al nivel del mar en metros con lectura digital en 34pt.
* **Rumbo y Brújula:** Dirección de avance en grados (0° a 359°) y punto cardinal completo en tiempo real (`NORTE`, `NORESTE`, `ESTE`, `SURESTE`, `SUR`, `SUROESTE`, `OESTE`, `NOROESTE`).
* **Satélites GPS en Grande:** Icono satelital vectorizado a gran escala, número de satélites en seguimiento en 34pt, índice de precisión HDOP y diagnóstico de señal.
* **Coordenadas Geográficas:** Latitud y Longitud con precisión de 5 decimales.
* **Transiciones Fluidas:** Navegación táctil deslizante (*Swipe*) o desvanecida (*Fade*) entre las 4 pantallas mediante indicador de páginas `[● ○ ○ ○]`, `[○ ● ○ ○]`, `[○ ○ ● ○]`, `[○ ○ ○ ●]`.
### 7. Gestión Inteligente de Energía para GPS (Comandos UBX)
* Integración total con el sistema de **Auto-Sleep**:
* Al suspender el sistema por inactividad o parada de motor, transmite la trama binaria `UBX-RXM-PMREQ` para apagar la etapa de radiofrecuencia del módulo GPS (consumo inferior a 0.5 mA).
* Al reactivarse por toque en pantalla o botón, envía una ráfaga serie *Wake-on-UART* para reanudar el seguimiento satelital de inmediato.
### 7. Audio y Melodías de Alarma
### 4. Audio y Melodías de Alarma
* Control de volumen independiente: Silencio, Ultra Bajo, Bajo (Casio suave), Medio y Alto.
* Selector de sonidos y tonos de alarma:
* Chime BMW (doble tono sincronizado).
@ -60,7 +35,7 @@ El sistema fue diseñado y calibrado a medida para el motor **Ford Ka Endura-E 1
* Tono bitonal clásico.
* Alarma continua urgente.
### 8. Menú de Configuración Táctil
### 5. Menú de Configuración Táctil
* **Gestión de Alarmas:** Conmutadores táctiles independientes ON/OFF para cada parámetro con guardado persistente en NVS (Preferences).
* **Temas Visuales:** 6 paletas de color con degradados ambientales de fondo (*Cyan, Emerald, Amber, Violet, Ice Blue, Graphite*).
* **Brillo Automático:** Regulación inteligente mediante sensor de luz ambiente (LDR) integrado en la placa, o selección manual (5% a 100%).
@ -75,18 +50,11 @@ El sistema fue diseñado y calibrado a medida para el motor **Ford Ka Endura-E 1
* Pantalla táctil resistiva con controlador XPT2046.
* Sensor de luz LDR en pin analógico.
* LED RGB integrado.
2. **Módulo GPS Satelital N32+ / U-blox NEO-7M / NEO-6M:**
* Conexión: Ficha de 4 pines al conector rotulado **UART** (o P1).
* Pin 1 (Vin 5V), Pin 2 (GPIO 1 TX0 hacia RX GPS), Pin 3 (GPIO 3 RX0 desde TX GPS), Pin 4 (GND).
* Comunicación: UART0 Hardware Serial nativo a 9600 bps (8N1).
* Nota para programación: Al flashear por cable USB, desconectar la ficha de 4 pines del GPS para evitar colisión con el grabador USB.
3. **Escáner OBD-II Bluetooth:**
2. **Escáner OBD-II Bluetooth:**
* Adaptador ELM327 v1.5 (muy recomendado con chip PIC18F25K80, evitar clones v2.1 de baja calidad).
4. **Sensores de Temperatura Digitales DS18B20:**
* Hasta 2 sensores en bus 1-Wire conectado a Pin 3 del conector CN1 (`GPIO 22`).
5. **Buzzer / Parlante:**
3. **Buzzer / Parlante:**
* Parlantito o buzzer pasivo de 8 ohms conectado al pin de audio del CYD (controlado por canal PWM con modulación de volumen).
6. **Alimentación:**
4. **Alimentación:**
* Módulo convertidor 12V a 5V USB (conectado a positivo bajo llave o directo si se utiliza auto-sleep).
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -37,29 +37,3 @@ Script en Python que emula un adaptador OBD-II Bluetooth ELM327 v1.5 completo y
- `010B`: Presión absoluta del múltiple (MAP)
- `0104`: Carga del motor (Engine Load)
- `221E1C` / `015C`: Temperatura de aceite y fluido de transmisión Ford
---
## Simulación de GPS NMEA por USB Serie (/dev/ttyUSB0)
La pantalla CYD recibe datos GPS a través de su puerto UART0 (`Serial` a 9600 bps), el cual está conectado internamente al conector USB (chip CH340). Esto permite simular un módulo GPS real directamente desde la PC **sin tocar ni una sola línea del firmware del ESP32**.
### Opción A: Simulador GPS Independiente (Solo GPS sin Bluetooth)
Ideal para probar las pantallas de telemetría GPS, reloj satelital, brújula y velocímetro sin necesidad de Bluetooth:
```bash
# Modo dinámico (aceleración, velocidad crucero y frenado automático):
python3 emulator/gps_emulator.py
# O con velocidad fija en km/h:
python3 emulator/gps_emulator.py --speed 75.0
```
### Opción B: Integrado con el Emulador OBD-II
Si ejecutas `emulator/obd2_emulator.py` con la CYD conectada por USB, detecta automáticamente `/dev/ttyUSB0` y transmite las tramas NMEA ($GPRMC, $GPGGA, $GPVTG) **sincronizadas exactamente con la velocidad del motor/ECU**:
```bash
python3 emulator/obd2_emulator.py
```
*Si deseas desactivar la simulación GPS al correr el emulador OBD-II, puedes usar `python3 emulator/obd2_emulator.py --no-gps`.*

View file

@ -1,149 +0,0 @@
#!/usr/bin/env python3
"""
Simulador GPS NMEA para ESP32 CYD (Cluster Ford Ka)
Transmite sentencias NMEA 0183 ($GPRMC, $GPGGA, $GPVTG) a 9600 bps
a través del puerto serie USB (/dev/ttyUSB0).
Permite probar y calibrar la telemetría GPS de la pantalla CYD
(Velocímetro GPS, Reloj Satelital, Brújula, Altitud, Coordenadas, Satélites)
sin requerir un módulo GPS físico ni modificar el firmware del ESP32.
"""
import sys
import time
import math
import serial
import argparse
from datetime import datetime, timezone
def nmea_checksum(sentence: str) -> str:
"""Calcula el checksum XOR estándar NMEA 0183"""
c = 0
for ch in sentence:
c ^= ord(ch)
return f"{c:02X}"
def build_nmea(body: str) -> bytes:
"""Envuelve la trama con '$' y '*CHECKSUM\\r\\n'"""
chk = nmea_checksum(body)
return f"${body}*{chk}\r\n".encode("ascii")
def run_gps_emulator(port="/dev/ttyUSB0", baud=9600, rate_hz=2.0, speed_sim=True, fixed_speed=70.0):
print("=" * 60)
print(" SIMULADOR GPS NMEA PARA ESP32 CYD")
print("=" * 60)
print(f"Puerto: {port}")
print(f"Baudrate: {baud} bps (8N1)")
print(f"Tasa: {rate_hz} Hz ({1.0/rate_hz:.2f}s por trama)")
print(f"Modo: {'Ciclo dinámico de velocidad' if speed_sim else f'Velocidad fija {fixed_speed} km/h'}")
print("-" * 60)
try:
ser = serial.Serial()
ser.port = port
ser.baudrate = baud
ser.timeout = 1
ser.dtr = False
ser.rts = False
ser.open()
print(f"[OK] Conectado exitosamente a {port}")
except Exception as e:
print(f"[ERROR] No se pudo abrir {port}: {e}")
print("Verifica que la placa CYD esté conectada por USB y que tengas permisos.")
sys.exit(1)
# Coordenadas iniciales simuladas (Buenos Aires, Argentina)
# -34.6037 S, -58.3816 W
base_lat = -34.6037
base_lon = -58.3816
altitude = 35.0 # metros
heading = 45.0 # grados (Noreste)
satellites = 9
t_start = time.time()
interval = 1.0 / rate_hz
try:
while True:
t = time.time()
dt = t - t_start
# 1. Simulación de velocidad
if speed_sim:
# Ciclo de 30 segundos: aceleración, crucero, desaceleración
phase = dt % 30.0
if phase < 5.0:
current_speed = 0.0
elif phase < 15.0:
# Acelera de 0 a 85 km/h
current_speed = (phase - 5.0) / 10.0 * 85.0
elif phase < 22.0:
# Crucero a 85 km/h con pequeña variación
current_speed = 85.0 + math.sin(phase) * 3.0
else:
# Frena de 85 a 0 km/h
current_speed = max(0.0, 85.0 * (1.0 - (phase - 22.0) / 8.0))
else:
current_speed = fixed_speed
# Variación suave del rumbo según movimiento
if current_speed > 1.0:
heading = (heading + 1.5 * interval) % 360.0
# Velocidad en nudos para NMEA (1 km/h = 0.539957 nudos)
speed_knots = current_speed * 0.539957
# 2. Hora y fecha UTC actual
now = datetime.now(timezone.utc)
time_str = now.strftime("%H%M%S.00")
date_str = now.strftime("%d%m%y")
# Formato de coordenadas NMEA: DDMM.MMMM
lat_deg = int(abs(base_lat))
lat_min = (abs(base_lat) - lat_deg) * 60.0
lat_dir = "S" if base_lat < 0 else "N"
lat_nmea = f"{lat_deg:02d}{lat_min:07.4f}"
lon_deg = int(abs(base_lon))
lon_min = (abs(base_lon) - lon_deg) * 60.0
lon_dir = "W" if base_lon < 0 else "E"
lon_nmea = f"{lon_deg:03d}{lon_min:07.4f}"
# 3. Construcción de sentencias NMEA
# A. $GPRMC: Sentencia recomendada mínima (Hora, Estado A, Lat, Lon, Nudos, Rumbo, Fecha)
rmc = f"GPRMC,{time_str},A,{lat_nmea},{lat_dir},{lon_nmea},{lon_dir},{speed_knots:.1f},{heading:.1f},{date_str},,,A"
# B. $GPGGA: Datos de fijación GPS (Hora, Lat, Lon, Calidad 1, Satélites, HDOP, Altura)
gga = f"GPGGA,{time_str},{lat_nmea},{lat_dir},{lon_nmea},{lon_dir},1,{satellites:02d},0.9,{altitude:.1f},M,16.0,M,,"
# C. $GPVTG: Rumbo y velocidad respecto al fondo (Rumbo, T, Nudos, N, Km/h, K)
vtg = f"GPVTG,{heading:.1f},T,,M,{speed_knots:.1f},N,{current_speed:.1f},K,A"
# Enviar ráfaga NMEA al ESP32 CYD
ser.write(build_nmea(rmc))
ser.write(build_nmea(gga))
ser.write(build_nmea(vtg))
ser.flush()
# Feedback en consola en una sola línea interactiva
print(f"\r[GPS Sim] Satélites: {satellites} | Vel: {current_speed:5.1f} km/h | Rumbo: {heading:5.1f}° | Alt: {altitude:4.1f}m | Hora UTC: {now.strftime('%H:%M:%S')}", end="", flush=True)
time.sleep(interval)
except KeyboardInterrupt:
print("\n\n[INFO] Deteniendo simulador GPS...")
finally:
ser.close()
print("[OK] Puerto serie cerrado.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Simulador GPS NMEA para ESP32 CYD")
parser.add_argument("--port", default="/dev/ttyUSB0", help="Puerto serie de la placa CYD (default: /dev/ttyUSB0)")
parser.add_argument("--baud", type=int, default=9600, help="Baudrate serie (default: 9600)")
parser.add_argument("--rate", type=float, default=2.0, help="Frecuencia en Hz (default: 2.0 Hz)")
parser.add_argument("--speed", type=float, default=None, help="Velocidad fija en km/h (desactiva ciclo)")
args = parser.parse_args()
sim_speed = (args.speed is None)
fix_spd = args.speed if args.speed is not None else 70.0
run_gps_emulator(port=args.port, baud=args.baud, rate_hz=args.rate, speed_sim=sim_speed, fixed_speed=fix_spd)

View file

@ -359,79 +359,6 @@ def handle_client_stream(sock):
print("\n[OBD2] Sesión cerrada.")
def nmea_checksum(sentence: str) -> str:
"""Calcula el checksum XOR estándar NMEA 0183"""
c = 0
for ch in sentence:
c ^= ord(ch)
return f"{c:02X}"
def gps_simulation_worker(port="/dev/ttyUSB0", baud=9600):
"""Hilo secundario para transmitir sentencias NMEA al ESP32 CYD vía USB serie"""
try:
import serial
from datetime import datetime, timezone
ser = serial.Serial()
ser.port = port
ser.baudrate = baud
ser.timeout = 1
ser.dtr = False
ser.rts = False
ser.open()
print(f"[GPS Sim] Puerto serie {port} abierto a {baud} bps. Transmitiendo NMEA sincronizado con ECU.")
except Exception as e:
print(f"[GPS Sim] Aviso: No se pudo abrir {port} para GPS ({e}).")
return
base_lat = -34.6037
base_lon = -58.3816
altitude = 35.0
heading = 45.0
satellites = 9
try:
while True:
with state.lock:
current_speed = state.speed
if current_speed > 1.0:
heading = (heading + 0.8) % 360.0
speed_knots = current_speed * 0.539957
now = datetime.now(timezone.utc)
time_str = now.strftime("%H%M%S.00")
date_str = now.strftime("%d%m%y")
lat_deg = int(abs(base_lat))
lat_min = (abs(base_lat) - lat_deg) * 60.0
lat_dir = "S" if base_lat < 0 else "N"
lat_nmea = f"{lat_deg:02d}{lat_min:07.4f}"
lon_deg = int(abs(base_lon))
lon_min = (abs(base_lon) - lon_deg) * 60.0
lon_dir = "W" if base_lon < 0 else "E"
lon_nmea = f"{lon_deg:03d}{lon_min:07.4f}"
rmc = f"GPRMC,{time_str},A,{lat_nmea},{lat_dir},{lon_nmea},{lon_dir},{speed_knots:.1f},{heading:.1f},{date_str},,,A"
gga = f"GPGGA,{time_str},{lat_nmea},{lat_dir},{lon_nmea},{lon_dir},1,{satellites:02d},0.9,{altitude:.1f},M,16.0,M,,"
vtg = f"GPVTG,{heading:.1f},T,,M,{speed_knots:.1f},N,{current_speed:.1f},K,A"
for msg in [rmc, gga, vtg]:
raw = f"${msg}*{nmea_checksum(msg)}\r\n".encode("ascii")
ser.write(raw)
ser.flush()
time.sleep(0.5) # 2 Hz update rate
except Exception as e:
pass
finally:
try:
ser.close()
except Exception:
pass
def simulation_worker():
"""Background thread to continuously update simulated values"""
while True:
@ -443,10 +370,6 @@ def run_emulator():
parser = argparse.ArgumentParser(description="OBD-II Bluetooth Emulator con soporte ECO Badge (Ford Ka Endura 1.3)")
parser.add_argument("--mode", "-m", choices=["cycle", "eco"], default="cycle", help="Modo inicial: 'cycle' (demostración) o 'eco' (fijo)")
parser.add_argument("--eco", "-e", action="store_true", help="Iniciar directamente en modo ECO fijo (RPM=2250, Carga=58%%)")
parser.add_argument("--gps", action="store_true", help="Forzar habilitación de simulación GPS por cable USB")
parser.add_argument("--no-gps", action="store_true", help="Desactivar simulación GPS por cable USB")
parser.add_argument("--gps-port", default="/dev/ttyUSB0", help="Puerto serie para simulación GPS (default: /dev/ttyUSB0)")
parser.add_argument("--gps-baud", type=int, default=9600, help="Baudrate para simulación GPS (default: 9600)")
args = parser.parse_args()
if args.eco or args.mode == "eco":
@ -469,15 +392,6 @@ def run_emulator():
sim_thread = threading.Thread(target=simulation_worker, daemon=True)
sim_thread.start()
# Start GPS simulation thread if port exists or --gps requested
if not args.no_gps and (args.gps or os.path.exists(args.gps_port)):
gps_thread = threading.Thread(
target=gps_simulation_worker,
args=(args.gps_port, args.gps_baud),
daemon=True
)
gps_thread.start()
# Set up D-Bus loop for BlueZ SPP registration
DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,003 KiB

View file

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<style>
.sat-body { fill: #FFFFFF; stroke: none; }
.sat-wave { fill: none; stroke: #FFFFFF; stroke-linecap: round; }
</style>
</defs>
<!-- Background container (optional preview) -->
<rect width="256" height="256" rx="24" fill="#1A365D"/>
<g id="gps-satellite-icon">
<!-- SATELLITE BODY: Central sphere + 2 diagonal panels (always fixed) -->
<g id="satellite-body" class="sat-body">
<!-- Center core sphere -->
<circle id="satellite-core" cx="100.00" cy="88.00" r="28.53"/>
<!-- Top-Right Solar Panel -->
<rect id="panel-top-right"
x="-25.60" y="-14.63"
width="51.20" height="29.26"
rx="7.31" ry="7.31"
transform="translate(100.00, 88.00) rotate(-34.0) translate(61.44, 0)"/>
<!-- Bottom-Left Solar Panel -->
<rect id="panel-bottom-left"
x="-25.60" y="-14.63"
width="51.20" height="29.26"
rx="7.31" ry="7.31"
transform="translate(100.00, 88.00) rotate(-34.0) translate(-61.44, 0)"/>
</g>
<!-- WAVE 1: First radio wave arc -->
<path id="wave-1" class="sat-wave"
d="M 167.20 84.48 A 67.29 67.29 0 0 1 86.01 153.82"
stroke-width="24.14"/>
<!-- WAVE 2: Second radio wave arc -->
<path id="wave-2" class="sat-wave"
d="M 213.95 82.03 A 114.10 114.10 0 0 1 76.28 199.61"
stroke-width="24.87"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

View file

@ -1,180 +0,0 @@
# Integración Módulo GPS N32+ con Modo Reposo por Comandos UBX
## Placa ESP32-2432S028 (CYD)
Este documento detalla la conexión física, el protocolo binario para poner a dormir y despertar el módulo GPS N32+ (U-blox) mediante comandos serie, y el esquema de integración con el firmware del instrumental OBD-II.
---
## 1. Especificaciones y Pinout del Módulo N32+ GPS
![Pinout del Módulo GPS U-blox N32+](pinout_gps.png)
* **Chipset**: U-blox 7 / NEO-7M (o compatible).
* **Protocolo de salida**: NMEA 0183 (mensajes $GPRMC, $GPGGA, $GPVTG).
* **Protocolo de configuración**: Binario U-blox (UBX).
* **Velocidad predeterminada (Baudrate)**: 9600 bps (8N1).
* **Voltaje de alimentación**: 5V (conectado a `Vin` del conector P1).
* **Nivel lógico UART**: 3.3V (compatible directo con ESP32 sin conversores).
* **Consumo activo**: ~45 a 60 mA.
* **Consumo en modo reposo (Software Standby / Backup)**: ~0.3 a 0.5 mA.
---
## 2. Esquema de Conexión Física (Bidireccional)
![Diagrama de conexión GPS y Sensores a la Placa CYD](diagrama_conexion_hardware.png)
Para recibir datos de navegación y al mismo tiempo poder enviar comandos de suspensión y despertar, se utiliza la ficha de 4 pines del conector rotulado como **UART** (o **P1**) en la parte trasera de la placa CYD:
| Pin del GPS N32+ | Pin en Conector UART (P1) | Función | Notas |
| :--- | :--- | :--- | :--- |
| **VCC / V+EN** (Naranja) | **Pin 1** (`Vin` 5V) | Alimentación | 5V directo del regulador/USB |
| **RX** (Blanco / Amarillo) | **Pin 2** (`GPIO 1` / TX0) | TX del ESP32 | Salida serial para enviar comandos UBX de reposo |
| **TX** (Azul / Verde) | **Pin 3** (`GPIO 3` / RX0) | RX del ESP32 | Entrada serial UART0 (sentencias NMEA a 9600 bps) |
| **GND** (Verde / Negro) | **Pin 4** (`GND`) | Masa común | Retorno de corriente |
> **Programación por cable USB**:
> Dado que el conector UART comparte internamente las líneas GPIO 1 y GPIO 3 con el chip conversor USB-Serie (CH340), **desconectar la ficha de 4 pines del GPS al flashear nuevo firmware por cable USB** para evitar interferencias en la carga. Una vez completada la carga, reconectar la ficha para el funcionamiento normal.
---
## 3. Comandos Binarios UBX de Reposo y Despertar
El chip U-blox cuenta con el mensaje binario **`UBX-RXM-PMREQ`** (Class `0x02`, ID `0x41`), que ordena la desconexión inmediata de la etapa de radiofrecuencia y procesado satelital.
### A. Trama para poner a dormir al GPS (Sleep / Backup Mode)
Estructura de 16 bytes con sincronismo, encabezado, longitud, payload y checksum Fletcher:
```text
Header: 0xB5 0x62 (Sync UBX)
Class: 0x02 (RXM)
ID: 0x41 (PMREQ)
Length: 0x08 0x00 (8 bytes de payload)
Payload: 0x00 0x00 0x00 0x00 (Duración = 0: suspensión indefinida hasta despertar)
0x02 0x00 0x00 0x00 (Flags: modo Backup / Software Standby)
Checksum: 0x4D 0x3B (CK_A, CK_B)
```
**Array en C++ listo para enviar**:
```cpp
const uint8_t UBX_SLEEP_CMD[] = {
0xB5, 0x62, 0x02, 0x41, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00,
0x4D, 0x3B
};
```
### B. Secuencia para despertar al GPS (Wake-up)
U-blox admite el despertar por actividad en la línea serie (**Wake on UART**). Basta con transmitir una ráfaga corta de bytes (por ejemplo `0xFF`) para que el receptor reactive sus osciladores y vuelva al modo activo.
```cpp
void gpsWake() {
// Envío de ráfaga de pulsos para despertar el receptor U-blox
uint8_t wakeBuffer[16];
memset(wakeBuffer, 0xFF, sizeof(wakeBuffer));
gpsSerial.write(wakeBuffer, sizeof(wakeBuffer));
gpsSerial.flush();
}
```
---
## 4. Código Modelo para Integrar en el Firmware
```cpp
#include <HardwareSerial.h>
#include <TinyGPSPlus.h>
#define GPS_RX_PIN 3 // Conector UART Pin 3 (RX0 ESP32, entrada desde TX del GPS)
#define GPS_TX_PIN 1 // Conector UART Pin 2 (TX0 ESP32, salida hacia RX del GPS)
#define GPS_BAUDRATE 9600
#define gpsSerial Serial // Usamos UART0 por hardware nativo
TinyGPSPlus gps;
bool gpsIsSleeping = false;
// Inicialización del puerto serie para GPS
void setupGPS() {
pinMode(GPS_RX_PIN, INPUT_PULLUP);
gpsSerial.begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
}
// Poner el GPS en modo reposo de ultra bajo consumo
void gpsSleep() {
if (gpsIsSleeping) return;
// Trama UBX-RXM-PMREQ para modo Backup permanente
const uint8_t ubxSleep[] = {
0xB5, 0x62, 0x02, 0x41, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00,
0x4D, 0x3B
};
gpsSerial.write(ubxSleep, sizeof(ubxSleep));
gpsSerial.flush();
gpsIsSleeping = true;
Serial.println(">>> [GPS] Modulo GPS puesto en modo reposo (Sleep).");
}
// Despertar el GPS
void gpsWake() {
if (!gpsIsSleeping) return;
// Enviar ráfaga de sincronismo en línea serie para disparar el evento Wake-on-UART
for (uint8_t i = 0; i < 32; i++) {
gpsSerial.write(0xFF);
}
gpsSerial.flush();
gpsIsSleeping = false;
Serial.println(">>> [GPS] Modulo GPS despertado.");
}
// Lectura continua en el bucle principal
void processGPS() {
if (gpsIsSleeping) return;
while (gpsSerial.available() > 0) {
gps.encode(gpsSerial.read());
}
}
```
---
## 5. Integración con el Sistema de Reposo Existente
En el archivo `sketch/obd2_gauge/config.h`:
1. **Al entrar en suspensión (`enterLightSleep`)**:
```cpp
gpsSleep(); // Apaga la etapa de radio y antena del GPS
```
2. **Al salir de la suspensión (`wokeFromSleep`)**:
```cpp
gpsWake(); // Reactiva el receptor U-blox
```
---
## 6. Funcionalidades a Implementar Posteriormente
1. **Velocidad Real vs Velocidad OBD-II**:
- Lectura: `gps.speed.kmph()`.
- Comparación directa con la velocidad reportada por la ECU (PID `010D`).
2. **Cronómetro 0 a 100 km/h (Performance Meter)**:
- Detección automática de detención (`speed == 0`).
- Disparo del cronómetro al detectar movimiento (`speed > 1.0 km/h`).
- Registro del tiempo exacto en milisegundos al cruzar los `100.0 km/h`.
3. **Hora Satelital Atómica y Fecha**:
- Lectura de `gps.time.hour()`, `gps.time.minute()`, `gps.time.second()`.
- Ajuste de huso horario (`hora - 3` para huso horario UTC-3).
4. **Altitud y Rumbo**:
- `gps.altitude.meters()` para altitud msnm.
- `gps.course.deg()` para orientación y brújula digital.

View file

@ -1,131 +0,0 @@
#!/usr/bin/env python3
"""
Toma la foto real enviada por el usuario (gps_pinout.jpg) y genera
una imagen anotada con el pinout exacto, colores de cables soldados
y su conexion a los conectores P1 y P3 de la placa ESP32 CYD.
"""
from PIL import Image, ImageDraw, ImageFont
import os
# 1. Cargar y recortar la foto original
src = Image.open("hardware/gps_pinout.jpg")
sw, sh = src.size
# Queremos enfocar el módulo y los cables
# El módulo está entre x = 0.2 y 0.85
crop_box = (int(sw * 0.15), 0, int(sw * 0.85), sh)
cropped = src.crop(crop_box)
# Redimensionar la foto recortada a un tamaño adecuado para el panel izquierdo (ancho 600)
target_h = 680
target_w = int(cropped.width * (target_h / cropped.height))
cropped_resized = cropped.resize((target_w, target_h), Image.Resampling.LANCZOS)
# Dimensiones finales del canvas
W = target_w + 580
H = 760
im = Image.new("RGB", (W, H), (11, 15, 25))
draw = ImageDraw.Draw(im)
# Pegar la foto en el lateral izquierdo
photo_x = 30
photo_y = 65
im.paste(cropped_resized, (photo_x, photo_y))
# Borde decorativo alrededor de la foto
draw.rectangle([photo_x - 1, photo_y - 1, photo_x + target_w, photo_y + target_h], outline=(50, 70, 110), width=2)
# Cargar fuentes
try:
f_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)
f_sub = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 13)
f_sec = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 15)
f_bold = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 13)
f_reg = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
f_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
except Exception:
f_title = f_sub = f_sec = f_bold = f_reg = f_small = ImageFont.load_default()
# -------------------------------------------------------------
# ENCABEZADO
# -------------------------------------------------------------
draw.text((30, 15), "PINOUT IDENTIFICADO DE TU MODULO GPS (FOTO REAL)", fill=(255, 255, 255), font=f_title)
draw.text((30, 40), "Correspondencia de tus cables soldados con la placa ESP32 CYD (ESP32-2432S028)", fill=(0, 210, 255), font=f_sub)
# -------------------------------------------------------------
# PANEL DERECHO: DETALLE DE PINES Y CABLES
# -------------------------------------------------------------
rx = photo_x + target_w + 25
rw = W - rx - 30
def draw_card(box, fill, outline, radius=6):
draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=1)
# Tarjeta 1: Cable Naranja (VCC y EN)
c1_box = [rx, photo_y, rx + rw, photo_y + 115]
draw_card(c1_box, (24, 30, 46), (220, 90, 30))
draw.rectangle([rx, photo_y, rx + 10, photo_y + 115], fill=(255, 120, 20))
draw.text((rx + 22, photo_y + 10), "1. CABLE NARANJA --> PADS 'V' y 'EN'", fill=(255, 140, 40), font=f_sec)
draw.text((rx + 22, photo_y + 32), "• Señal: VCC (Alimentación 5V) + Enable activado.", fill=(220, 225, 235), font=f_reg)
draw.text((rx + 22, photo_y + 52), " (Tu foto muestra el pad 'EN' puenteado con estaño a 'V').", fill=(160, 175, 195), font=f_small)
draw.text((rx + 22, photo_y + 75), "• CONECTAR A:", fill=(255, 215, 60), font=f_bold)
draw.text((rx + 145, photo_y + 75), "PIN 1 del Conector P1 (Vin 5V)", fill=(255, 255, 255), font=f_bold)
draw.text((rx + 22, photo_y + 93), " Entrega 5V directo desde el puerto USB.", fill=(160, 175, 195), font=f_small)
# Tarjeta 2: Cable Azul (TX)
c2_box = [rx, photo_y + 128, rx + rw, photo_y + 243]
draw_card(c2_box, (24, 30, 46), (40, 120, 220))
draw.rectangle([rx, photo_y + 128, rx + 10, photo_y + 243], fill=(35, 130, 245))
draw.text((rx + 22, photo_y + 138), "2. CABLE AZUL --> PAD 'T' (TX)", fill=(60, 160, 255), font=f_sec)
draw.text((rx + 22, photo_y + 160), "• Señal: TX (Salida de datos NMEA del GPS al ESP32).", fill=(220, 225, 235), font=f_reg)
draw.text((rx + 22, photo_y + 180), " Transmite velocidad, satélites, hora, rumbo y coordenadas.", fill=(160, 175, 195), font=f_small)
draw.text((rx + 22, photo_y + 203), "• CONECTAR A:", fill=(255, 215, 60), font=f_bold)
draw.text((rx + 145, photo_y + 203), "PIN 3 del Conector P3 (GPIO 35)", fill=(255, 255, 255), font=f_bold)
draw.text((rx + 22, photo_y + 221), " Entrada RX1 por hardware de la placa CYD.", fill=(160, 175, 195), font=f_small)
# Tarjeta 3: Cable Blanco (RX)
c3_box = [rx, photo_y + 256, rx + rw, photo_y + 371]
draw_card(c3_box, (24, 30, 46), (180, 185, 200))
draw.rectangle([rx, photo_y + 256, rx + 10, photo_y + 371], fill=(230, 235, 245))
draw.text((rx + 22, photo_y + 266), "3. CABLE BLANCO --> PAD 'R' (RX)", fill=(240, 245, 255), font=f_sec)
draw.text((rx + 22, photo_y + 288), "• Señal: RX (Entrada de comandos al GPS desde el ESP32).", fill=(220, 225, 235), font=f_reg)
draw.text((rx + 22, photo_y + 308), " Permite poner el GPS en modo Sleep y reactivarlo.", fill=(160, 175, 195), font=f_small)
draw.text((rx + 22, photo_y + 331), "• CONECTAR A:", fill=(255, 215, 60), font=f_bold)
draw.text((rx + 145, photo_y + 331), "PIN 2 del Conector P1 (GPIO 1 / TX0)", fill=(255, 255, 255), font=f_bold)
draw.text((rx + 22, photo_y + 349), " Salida de comandos de reposo del instrumental.", fill=(160, 175, 195), font=f_small)
# Tarjeta 4: Cable Verde (GND)
c4_box = [rx, photo_y + 384, rx + rw, photo_y + 499]
draw_card(c4_box, (24, 30, 46), (30, 170, 90))
draw.rectangle([rx, photo_y + 384, rx + 10, photo_y + 499], fill=(40, 200, 110))
draw.text((rx + 22, photo_y + 394), "4. CABLE VERDE --> PAD 'G' (GND)", fill=(50, 215, 120), font=f_sec)
draw.text((rx + 22, photo_y + 416), "• Señal: GND (Masa / Retorno común de corriente).", fill=(220, 225, 235), font=f_reg)
draw.text((rx + 22, photo_y + 436), " Referencia de masa para alimentación y bus serie.", fill=(160, 175, 195), font=f_small)
draw.text((rx + 22, photo_y + 459), "• CONECTAR A:", fill=(255, 215, 60), font=f_bold)
draw.text((rx + 145, photo_y + 459), "PIN 4 del Conector P1 (GND)", fill=(255, 255, 255), font=f_bold)
draw.text((rx + 22, photo_y + 477), " (O bien al Pin 2 del conector P3).", fill=(160, 175, 195), font=f_small)
# Tarjeta 5: Pad VB (Sin conectar)
c5_box = [rx, photo_y + 512, rx + rw, photo_y + 610]
draw_card(c5_box, (18, 22, 34), (60, 75, 100))
draw.rectangle([rx, photo_y + 512, rx + 10, photo_y + 610], fill=(120, 130, 150))
draw.text((rx + 22, photo_y + 522), "5. PAD 'VB' (V_BACKUP) --> SIN CONECTAR", fill=(170, 185, 205), font=f_bold)
draw.text((rx + 22, photo_y + 544), "• Este pad dorado está abajo de todo, junto a la pila botón.", fill=(140, 155, 175), font=f_small)
draw.text((rx + 22, photo_y + 564), "• No requiere conexión: Tu módulo ya tiene la pila recargable", fill=(140, 155, 175), font=f_small)
draw.text((rx + 22, photo_y + 584), " soldada en la placa para retener la memoria satelital.", fill=(140, 155, 175), font=f_small)
# Tarjeta inferior de resumen
c6_box = [rx, photo_y + 623, rx + rw, photo_y + 680]
draw_card(c6_box, (28, 24, 16), (220, 170, 40))
draw.text((rx + 18, photo_y + 632), "CONSEJO:", fill=(255, 200, 50), font=f_bold)
draw.text((rx + 95, photo_y + 632), "Al flashear por USB el CYD, si el cable blanco (RX)", fill=(240, 240, 240), font=f_small)
draw.text((rx + 18, photo_y + 652), "está enchufado a GPIO 1, desconéctalo temporalmente para evitar conflicto.", fill=(240, 240, 240), font=f_small)
# Guardar
out1 = "hardware/pinout_gps_foto_anotada.png"
out2 = "pinout_gps_foto_anotada.png"
im.save(out1, "PNG")
im.save(out2, "PNG")
print("Imagen generada con éxito:", out1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View file

@ -1,305 +0,0 @@
#!/usr/bin/env python3
"""
Genera el diagrama de conexión completo para la placa ESP32 CYD:
- 2x Sensores de temperatura DS18B20 en conector CN1 (1-Wire GPIO 22)
- 1x Módulo GPS N32+ / U-blox en conectores P1 y P3 (5V, GND, GPIO 35 RX, GPIO 1 TX)
"""
from PIL import Image, ImageDraw, ImageFont
import os
W, H = 1060, 750
im = Image.new("RGB", (W, H), (11, 15, 25)) # Sleek dark navy
draw = ImageDraw.Draw(im)
# Load fonts
try:
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)
font_subtitle = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
font_sec = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 13)
font_bold = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 11)
font_reg = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)
font_tiny = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 9)
except Exception:
font_title = ImageFont.load_default()
font_subtitle = font_sec = font_bold = font_reg = font_small = font_tiny = font_title
# Helper for drawing rounded boxes
def draw_card(box, fill, outline, radius=6):
draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=1)
# Color palette
C_BG = (11, 15, 25)
C_CARD = (20, 26, 42)
C_CARD_BORDER = (45, 58, 88)
C_CARD_HEADER = (28, 36, 58)
C_TEXT_WHITE = (240, 243, 248)
C_TEXT_MUTED = (140, 153, 175)
C_TEXT_CYAN = (0, 210, 255)
# Wire Colors
COLOR_VCC_3V3 = (255, 75, 75) # Red (3.3V Sensores)
COLOR_GND_DS = (40, 130, 255) # Blue (GND Sensores)
COLOR_DATA_DS = (245, 195, 35) # Gold / Yellow (Datos Sensores)
# GPS Wire Colors (Matching User's physical soldered cable)
COLOR_GPS_VCC = (255, 120, 20) # Cable Naranja (Pad V y EN puenteados)
COLOR_GPS_TX = (35, 130, 245) # Cable Azul (Pad T / TX del GPS)
COLOR_GPS_RX = (235, 240, 250) # Cable Blanco (Pad R / RX del GPS)
COLOR_GPS_GND = (40, 200, 110) # Cable Verde (Pad G / GND del GPS)
# -------------------------------------------------------------
# 1. HEADER
# -------------------------------------------------------------
draw.text((35, 20), "DIAGRAMA DE CONEXION: SENSORES DS18B20 Y MODULO GPS A PLACA CYD", fill=C_TEXT_WHITE, font=font_title)
draw.text((35, 45), "ESP32-2432S028 (CYD 2.8\" TFT) - Conectores Traseros CN1, P1 (Alimentación / TX) y P3 (RX)", fill=C_TEXT_CYAN, font=font_subtitle)
draw.line([(35, 68), (W - 35, 68)], fill=(35, 46, 72), width=1)
# -------------------------------------------------------------
# 2. SECCIÓN IZQUIERDA ARRIBA: SENSORES DS18B20
# -------------------------------------------------------------
ds_panel_box = [35, 80, 520, 315]
draw_card(ds_panel_box, (15, 20, 34), C_CARD_BORDER, radius=8)
draw.text((50, 92), "1. BUS 1-WIRE: TEMPERATURA INTERIOR Y EXTERIOR", fill=(100, 215, 255), font=font_sec)
# Sensor 1 (Interior)
s1_box = [50, 120, 175, 195]
draw_card(s1_box, (24, 32, 50), (60, 75, 105), radius=5)
draw.text((70, 126), "SENSOR 1", fill=(100, 215, 255), font=font_bold)
draw.text((72, 142), "DS18B20", fill=C_TEXT_WHITE, font=font_reg)
draw.text((68, 158), "Habitaculo", fill=C_TEXT_MUTED, font=font_small)
draw.text((68, 172), "(Cara plana)", fill=(100, 115, 135), font=font_tiny)
# Sensor 2 (Exterior)
s2_box = [205, 120, 330, 195]
draw_card(s2_box, (24, 32, 50), (60, 75, 105), radius=5)
draw.text((225, 126), "SENSOR 2", fill=(100, 215, 255), font=font_bold)
draw.text((227, 142), "DS18B20", fill=C_TEXT_WHITE, font=font_reg)
draw.text((229, 158), "Exterior", fill=C_TEXT_MUTED, font=font_small)
draw.text((223, 172), "(Cara plana)", fill=(100, 115, 135), font=font_tiny)
# Pines Sensor 1
p1_s1 = (72, 195)
p2_s1 = (112, 195)
p3_s1 = (152, 195)
draw.text((65, 200), "P1:GND", fill=COLOR_GND_DS, font=font_tiny)
draw.text((106, 200), "P2:DQ", fill=COLOR_DATA_DS, font=font_tiny)
draw.text((144, 200), "P3:VCC", fill=COLOR_VCC_3V3, font=font_tiny)
# Pines Sensor 2
p1_s2 = (227, 195)
p2_s2 = (267, 195)
p3_s2 = (307, 195)
draw.text((220, 200), "P1:GND", fill=COLOR_GND_DS, font=font_tiny)
draw.text((261, 200), "P2:DQ", fill=COLOR_DATA_DS, font=font_tiny)
draw.text((299, 200), "P3:VCC", fill=COLOR_VCC_3V3, font=font_tiny)
# Resistencia 4.7k
res_box = [370, 235, 415, 265]
draw_card(res_box, (220, 200, 160), (140, 120, 80), radius=3)
# Bandas de color de 4.7k (Amarillo, Violeta, Rojo, Dorado)
draw.line([(380, 235), (380, 265)], fill=(240, 210, 20), width=3) # Amarillo (4)
draw.line([(388, 235), (388, 265)], fill=(150, 40, 180), width=3) # Violeta (7)
draw.line([(396, 235), (396, 265)], fill=(220, 30, 30), width=3) # Rojo (x100)
draw.line([(406, 235), (406, 265)], fill=(210, 175, 55), width=2) # Dorado (+-5%)
draw.text((352, 218), "Resistencia 4.7k", fill=C_TEXT_WHITE, font=font_bold)
draw.text((362, 272), "(Pull-up)", fill=C_TEXT_MUTED, font=font_small)
# Wires DS18B20:
# GND (Azul) - Pata 1 de ambos
draw.line([(p1_s1[0], 215), (p1_s1[0], 285)], fill=COLOR_GND_DS, width=2)
draw.line([(p1_s2[0], 215), (p1_s2[0], 285)], fill=COLOR_GND_DS, width=2)
draw.line([(p1_s1[0], 285), (540, 285)], fill=COLOR_GND_DS, width=2)
draw.ellipse([p1_s1[0]-3, 285-3, p1_s1[0]+3, 285+3], fill=COLOR_GND_DS)
draw.ellipse([p1_s2[0]-3, 285-3, p1_s2[0]+3, 285+3], fill=COLOR_GND_DS)
# DQ Datos (Amarillo) - Pata 2 de ambos
draw.line([(p2_s1[0], 215), (p2_s1[0], 255)], fill=COLOR_DATA_DS, width=2)
draw.line([(p2_s2[0], 215), (p2_s2[0], 255)], fill=COLOR_DATA_DS, width=2)
draw.line([(p2_s1[0], 255), (370, 255)], fill=COLOR_DATA_DS, width=2)
draw.line([(415, 255), (540, 255)], fill=COLOR_DATA_DS, width=2)
draw.ellipse([p2_s1[0]-3, 255-3, p2_s1[0]+3, 255+3], fill=COLOR_DATA_DS)
draw.ellipse([p2_s2[0]-3, 255-3, p2_s2[0]+3, 255+3], fill=COLOR_DATA_DS)
# VCC 3.3V (Rojo) - Pata 3 de ambos y conectado al otro extremo de resistencia 4.7k
draw.line([(p3_s1[0], 215), (p3_s1[0], 230)], fill=COLOR_VCC_3V3, width=2)
draw.line([(p3_s2[0], 215), (p3_s2[0], 230)], fill=COLOR_VCC_3V3, width=2)
draw.line([(p3_s1[0], 230), (540, 230)], fill=COLOR_VCC_3V3, width=2)
draw.ellipse([p3_s1[0]-3, 230-3, p3_s1[0]+3, 230+3], fill=COLOR_VCC_3V3)
draw.ellipse([p3_s2[0]-3, 230-3, p3_s2[0]+3, 230+3], fill=COLOR_VCC_3V3)
draw.line([(392, 235), (392, 230)], fill=COLOR_VCC_3V3, width=2)
draw.ellipse([392-3, 230-3, 392+3, 230+3], fill=COLOR_VCC_3V3)
# -------------------------------------------------------------
# 3. SECCIÓN IZQUIERDA ABAJO: MODULO GPS (FOTO REAL / PADS V, EN, T, R, G, VB)
# -------------------------------------------------------------
gps_panel_box = [35, 335, 520, 560]
draw_card(gps_panel_box, (15, 20, 34), C_CARD_BORDER, radius=8)
draw.text((50, 347), "2. MODULO GPS N32+ / U-BLOX (TUS CABLES SOLDADOS)", fill=(100, 255, 160), font=font_sec)
# Modulo GPS Box
gps_body_box = [50, 375, 255, 545]
draw_card(gps_body_box, (20, 38, 70), (40, 80, 140), radius=6)
draw.text((65, 382), "MODULO GPS", fill=C_TEXT_WHITE, font=font_bold)
draw.text((65, 396), "N32+ (Foto Real)", fill=C_TEXT_CYAN, font=font_tiny)
# Antena Cerámica cuadrada en el módulo
ant_box = [62, 415, 140, 495]
draw_card(ant_box, (180, 145, 60), (120, 95, 35), radius=4)
draw.ellipse([94, 448, 106, 460], fill=(230, 230, 230), outline=(100, 100, 100))
draw.text((65, 500), "Antena GPS", fill=C_TEXT_MUTED, font=font_tiny)
# Pila de botón abajo a la derecha
draw.ellipse([80, 516, 102, 538], fill=(195, 202, 212), outline=(130, 135, 145))
draw.text((108, 522), "VB: Pila Backup", fill=(140, 155, 175), font=font_tiny)
# Pines del GPS (en el lateral derecho del módulo x=255)
gps_p_vcc = (255, 395)
gps_p_tx = (255, 435)
gps_p_rx = (255, 470)
gps_p_gnd = (255, 505)
draw.text((150, 387), "V+EN (5V)", fill=COLOR_GPS_VCC, font=font_bold)
draw.text((150, 399), "[Naranja]", fill=COLOR_GPS_VCC, font=font_tiny)
draw.text((150, 427), "T: TX", fill=COLOR_GPS_TX, font=font_bold)
draw.text((150, 439), "[Azul]", fill=COLOR_GPS_TX, font=font_tiny)
draw.text((150, 462), "R: RX", fill=COLOR_GPS_RX, font=font_bold)
draw.text((150, 474), "[Blanco]", fill=(200, 205, 215), font=font_tiny)
draw.text((150, 497), "G: GND", fill=COLOR_GPS_GND, font=font_bold)
draw.text((150, 509), "[Verde]", fill=COLOR_GPS_GND, font=font_tiny)
# Puntos de conexión en el borde
draw.ellipse([gps_p_vcc[0]-4, gps_p_vcc[1]-4, gps_p_vcc[0]+4, gps_p_vcc[1]+4], fill=COLOR_GPS_VCC)
draw.ellipse([gps_p_tx[0]-4, gps_p_tx[1]-4, gps_p_tx[0]+4, gps_p_tx[1]+4], fill=COLOR_GPS_TX)
draw.ellipse([gps_p_rx[0]-4, gps_p_rx[1]-4, gps_p_rx[0]+4, gps_p_rx[1]+4], fill=COLOR_GPS_RX)
draw.ellipse([gps_p_gnd[0]-4, gps_p_gnd[1]-4, gps_p_gnd[0]+4, gps_p_gnd[1]+4], fill=COLOR_GPS_GND)
# Trazado de cables GPS hacia la derecha (todos al conector UART / P1)
# Cable Naranja (VCC 5V) -> va a Pin 1 de P1 (x=600, y=278)
draw.line([gps_p_vcc, (340, 395), (340, 278), (605, 278)], fill=COLOR_GPS_VCC, width=2)
# Cable Blanco (RX GPS) -> va a Pin 2 de P1 (x=600, y=298)
draw.line([gps_p_rx, (280, 470), (280, 298), (605, 298)], fill=COLOR_GPS_RX, width=2)
# Cable Azul (TX GPS) -> va a Pin 3 de P1 (x=600, y=318)
draw.line([gps_p_tx, (300, 435), (300, 318), (605, 318)], fill=COLOR_GPS_TX, width=2)
# Cable Verde (GND GPS) -> va a Pin 4 de P1 (x=600, y=338)
draw.line([gps_p_gnd, (320, 505), (320, 338), (605, 338)], fill=COLOR_GPS_GND, width=2)
# -------------------------------------------------------------
# 4. SECCIÓN DERECHA: PLACA ESP32 CYD (ESP32-2432S028)
# -------------------------------------------------------------
cyd_box = [570, 80, 1025, 560]
draw_card(cyd_box, (18, 24, 38), (240, 190, 40), radius=10)
draw.text((590, 92), "PLACA ESP32 CYD (ESP32-2432S028)", fill=(255, 215, 60), font=font_sec)
draw.text((590, 110), "Vista de Conectores Traseros JST 1.25mm", fill=C_TEXT_MUTED, font=font_small)
# --- CONECTOR CN1 (4 pines) ---
cn1_box = [590, 135, 1005, 235]
draw_card(cn1_box, (25, 34, 52), (50, 68, 100), radius=6)
draw.text((605, 142), "CONECTOR CN1 (Sensores Temperatura 1-Wire)", fill=(100, 215, 255), font=font_bold)
draw.rectangle([605, 160, 620, 172], fill=COLOR_VCC_3V3)
draw.text((630, 160), "Pin 1: 3.3V (VCC)", fill=C_TEXT_WHITE, font=font_reg)
draw.text((760, 160), "<-- Pata 3 de ambos DS18B20", fill=COLOR_VCC_3V3, font=font_small)
draw.rectangle([605, 177, 620, 189], fill=(70, 75, 85))
draw.text((630, 177), "Pin 2: (No usar / Libre)", fill=(130, 135, 145), font=font_small)
draw.rectangle([605, 194, 620, 206], fill=COLOR_DATA_DS)
draw.text((630, 194), "Pin 3: GPIO 22 (Datos)", fill=C_TEXT_WHITE, font=font_reg)
draw.text((785, 194), "<-- Pata 2 de ambos DS18B20 + 4.7k", fill=COLOR_DATA_DS, font=font_small)
draw.rectangle([605, 211, 620, 223], fill=COLOR_GND_DS)
draw.text((630, 211), "Pin 4: GND (Masa)", fill=C_TEXT_WHITE, font=font_reg)
draw.text((765, 211), "<-- Pata 1 de ambos DS18B20", fill=COLOR_GND_DS, font=font_small)
# Conexion fisica cables CN1
draw.line([(540, 230), (575, 230), (575, 166), (605, 166)], fill=COLOR_VCC_3V3, width=2)
draw.line([(540, 255), (570, 255), (570, 200), (605, 200)], fill=COLOR_DATA_DS, width=2)
draw.line([(540, 285), (565, 285), (565, 217), (605, 217)], fill=COLOR_GND_DS, width=2)
# --- CONECTOR P1 / UART (4 pines: Vin, TX0, RX0, GND) ---
p1_box = [590, 245, 1005, 370]
draw_card(p1_box, (25, 34, 52), (50, 68, 100), radius=6)
draw.text((605, 252), "CONECTOR UART / P1 (Conexion GPS Completa)", fill=(255, 180, 50), font=font_bold)
draw.rectangle([605, 272, 620, 284], fill=COLOR_GPS_VCC)
draw.text((630, 272), "Pin 1: Vin (5V USB)", fill=C_TEXT_WHITE, font=font_reg)
draw.text((760, 272), "<-- Cable Naranja (Pad V+EN GPS)", fill=COLOR_GPS_VCC, font=font_bold)
draw.rectangle([605, 292, 620, 304], fill=COLOR_GPS_RX)
draw.text((630, 292), "Pin 2: GPIO 1 (TX0)", fill=C_TEXT_WHITE, font=font_reg)
draw.text((760, 292), "--> Cable Blanco (Pad R/RX GPS)", fill=(235, 240, 250), font=font_bold)
draw.rectangle([605, 312, 620, 324], fill=COLOR_GPS_TX)
draw.text((630, 312), "Pin 3: GPIO 3 (RX0)", fill=C_TEXT_WHITE, font=font_reg)
draw.text((760, 312), "<-- Cable Azul (Pad T/TX GPS)", fill=COLOR_GPS_TX, font=font_bold)
draw.rectangle([605, 332, 620, 344], fill=COLOR_GPS_GND)
draw.text((630, 332), "Pin 4: GND (Masa)", fill=C_TEXT_WHITE, font=font_reg)
draw.text((760, 332), "<-- Cable Verde (Pad G/GND GPS)", fill=COLOR_GPS_GND, font=font_bold)
# --- CONECTOR P3 (4 pines: 3.3V, GND, GPIO 35, GPIO 21) ---
p3_box = [590, 380, 1005, 545]
draw_card(p3_box, (25, 34, 52), (50, 68, 100), radius=6)
draw.text((605, 387), "CONECTOR P3 (Libre / GPIOs Auxiliares)", fill=(100, 255, 160), font=font_bold)
draw.rectangle([605, 407, 620, 419], fill=(70, 75, 85))
draw.text((630, 407), "Pin 1: 3.3V (Libre)", fill=(130, 135, 145), font=font_small)
draw.rectangle([605, 427, 620, 439], fill=(70, 75, 85))
draw.text((630, 427), "Pin 2: GND (Masa auxiliar)", fill=(130, 135, 145), font=font_small)
draw.rectangle([605, 447, 620, 459], fill=(70, 75, 85))
draw.text((630, 447), "Pin 3: GPIO 35 (Libre)", fill=(130, 135, 145), font=font_small)
draw.rectangle([605, 467, 620, 479], fill=(70, 75, 85))
draw.text((630, 467), "Pin 4: GND / IO21 (Libre)", fill=(130, 135, 145), font=font_small)
# -------------------------------------------------------------
# 5. RESUMEN DE CONEXION RAPIDA (BLOQUE INFERIOR)
# -------------------------------------------------------------
summary_box = [35, 575, 1025, 730]
draw_card(summary_box, (15, 20, 32), (50, 65, 95), radius=8)
draw.text((50, 585), "TABLA RESUMEN DE CONEXIONES EXACTAS PARA TUS CABLES:", fill=C_TEXT_WHITE, font=font_bold)
# Columna 1: DS18B20
draw.rectangle([50, 608, 62, 620], fill=COLOR_GND_DS)
draw.text((70, 608), "Pata 1 Sensores (Masa) --> Pin 4 de CN1 (GND)", fill=C_TEXT_WHITE, font=font_small)
draw.rectangle([50, 628, 62, 640], fill=COLOR_DATA_DS)
draw.text((70, 628), "Pata 2 Sensores (Datos) --> Pin 3 de CN1 (GPIO 22) [+ Resistencia 4.7k a 3.3V]", fill=C_TEXT_WHITE, font=font_small)
draw.rectangle([50, 648, 62, 660], fill=COLOR_VCC_3V3)
draw.text((70, 648), "Pata 3 Sensores (VCC) --> Pin 1 de CN1 (3.3V Corriente)", fill=C_TEXT_WHITE, font=font_small)
# Columna 2: GPS (Todo al conector UART / P1)
col2_x = 540
draw.rectangle([col2_x, 608, col2_x + 12, 620], fill=COLOR_GPS_VCC)
draw.text((col2_x + 20, 608), "Cable NARANJA (Pads V + EN) --> Pin 1 de UART (Vin 5V USB Alimentación)", fill=C_TEXT_WHITE, font=font_small)
draw.rectangle([col2_x, 628, col2_x + 12, 640], fill=COLOR_GPS_RX)
draw.text((col2_x + 20, 628), "Cable BLANCO (Pad R / RX) --> Pin 2 de UART (GPIO 1 / TX0 Comandos Sleep)", fill=C_TEXT_WHITE, font=font_small)
draw.rectangle([col2_x, 648, col2_x + 12, 660], fill=COLOR_GPS_TX)
draw.text((col2_x + 20, 648), "Cable AZUL (Pad T / TX) --> Pin 3 de UART (GPIO 3 / RX0 Datos NMEA)", fill=C_TEXT_WHITE, font=font_small)
draw.rectangle([col2_x, 668, col2_x + 12, 680], fill=COLOR_GPS_GND)
draw.text((col2_x + 20, 668), "Cable VERDE (Pad G / GND) --> Pin 4 de UART (GND / Masa Común)", fill=C_TEXT_WHITE, font=font_small)
# Guardar en ambas ubicaciones
im.save("hardware/diagrama_conexion_ds18b20.png", "PNG")
im.save("diagrama_conexion_ds18b20.png", "PNG")
im.save("hardware/diagrama_conexion_hardware.png", "PNG")
print("Imagenes generadas exitosamente!")

View file

@ -1,216 +0,0 @@
#!/usr/bin/env python3
"""
Genera una ilustración técnica de alta resolución del pinout del módulo GPS
(U-blox NEO-6M / NEO-7M / N32+) y su correspondencia con la placa ESP32 CYD.
"""
from PIL import Image, ImageDraw, ImageFont
import os
W, H = 1000, 680
im = Image.new("RGB", (W, H), (11, 15, 25)) # Sleek dark navy
draw = ImageDraw.Draw(im)
# Load fonts
try:
font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)
font_subtitle = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
font_sec = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
font_bold = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 12)
font_reg = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)
font_tiny = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 9)
font_chip = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 10)
except Exception:
font_title = font_subtitle = font_sec = font_bold = font_reg = font_small = font_tiny = font_chip = ImageFont.load_default()
def draw_card(box, fill, outline, radius=6, width=1):
draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)
# Palette
C_CARD = (18, 24, 38)
C_CARD_BORDER = (45, 58, 88)
C_TEXT_WHITE = (245, 248, 252)
C_TEXT_MUTED = (145, 158, 180)
C_TEXT_CYAN = (0, 210, 255)
COLOR_VCC = (255, 60, 60)
COLOR_RX = (255, 145, 0)
COLOR_TX = (40, 215, 125)
COLOR_GND = (45, 135, 255)
COLOR_PPS = (190, 100, 255)
# -------------------------------------------------------------
# 1. HEADER
# -------------------------------------------------------------
draw.text((40, 20), "PINOUT Y GUIA DE CONEXION: MODULO GPS U-BLOX / N32+", fill=C_TEXT_WHITE, font=font_title)
draw.text((40, 46), "Chipsets NEO-6M / NEO-7M / NEO-8M / N32+ con Antena Cerámica y Puerto Serial UART", fill=C_TEXT_CYAN, font=font_subtitle)
draw.line([(40, 70), (W - 40, 70)], fill=(35, 46, 72), width=1)
# -------------------------------------------------------------
# 2. CUADRO PRINCIPAL DEL MODULO GPS (ILUSTRACION DE PLACA)
# -------------------------------------------------------------
board_panel = [40, 85, 480, 480]
draw_card(board_panel, (14, 18, 30), C_CARD_BORDER, radius=8)
draw.text((55, 95), "VISTA SUPERIOR DEL MODULO GPS", fill=C_TEXT_CYAN, font=font_sec)
# PCB del GPS (Azul industrial estilo U-blox)
pcb_box = [80, 130, 440, 440]
draw_card(pcb_box, (15, 50, 105), (35, 80, 155), radius=10, width=2)
# Orificios de montaje en las 4 esquinas (con arandela dorada)
for hx, hy in [(95, 145), (425, 145), (95, 425), (425, 425)]:
draw.ellipse([hx-7, hy-7, hx+7, hy+7], fill=(190, 160, 60), outline=(140, 110, 30))
draw.ellipse([hx-4, hy-4, hx+4, hy+4], fill=(11, 15, 25))
# Antena Cerámica Cuadrada (Parche cerámico color marrón dorado)
ant_box = [115, 165, 260, 310]
draw_card(ant_box, (185, 145, 65), (130, 95, 35), radius=6, width=2)
# Superficie metalizada superior de la antena
draw_card([125, 175, 250, 300], (210, 175, 95), (150, 115, 50), radius=4, width=1)
# Punto central de soldadura / alimentación RF
draw.ellipse([182, 232, 194, 244], fill=(245, 245, 245), outline=(120, 120, 120), width=1)
draw.text((135, 315), "ANTENA CERAMICA GPS", fill=(210, 185, 130), font=font_small)
# Blindaje Metálico U-blox (Shield Can)
shield_box = [280, 165, 415, 285]
draw_card(shield_box, (190, 198, 210), (140, 148, 160), radius=4, width=2)
draw.rectangle([282, 167, 413, 172], fill=(220, 228, 238)) # Reflejo de luz
draw.text((295, 178), "u-blox", fill=(190, 30, 30), font=font_sec)
draw.text((295, 202), "NEO-7M-0-001", fill=(30, 35, 45), font=font_chip)
draw.text((295, 218), "GPS / GLONASS", fill=(60, 70, 85), font=font_tiny)
# Mini QR / DataMatrix decorativo
draw_card([370, 235, 405, 270], (240, 240, 240), (80, 80, 80), radius=2)
draw.rectangle([374, 239, 380, 245], fill=(30, 30, 30))
draw.rectangle([395, 239, 401, 245], fill=(30, 30, 30))
draw.rectangle([374, 260, 380, 266], fill=(30, 30, 30))
draw.rectangle([386, 248, 392, 254], fill=(30, 30, 30))
# Pila de botón de respaldo (RTC / Ephemeris Backup Battery)
batt_x, batt_y = 305, 325
draw.ellipse([batt_x-16, batt_y-16, batt_x+16, batt_y+16], fill=(195, 202, 212), outline=(130, 135, 145), width=2)
draw.text((batt_x-10, batt_y-5), "3V", fill=(50, 55, 65), font=font_tiny)
draw.text((280, 345), "Pila RTC Backup", fill=C_TEXT_MUTED, font=font_tiny)
# LED de Estado PPS (Fix Indicator)
led_x, led_y = 380, 325
draw.ellipse([led_x-7, led_y-7, led_x+7, led_y+7], fill=(50, 220, 110), outline=(20, 140, 60), width=1)
draw.ellipse([led_x-3, led_y-3, led_x+3, led_y+3], fill=(200, 255, 220)) # brillo central
draw.text((358, 345), "LED PPS (Fix)", fill=(80, 235, 130), font=font_tiny)
# Silkscreen del modelo
draw.text((120, 350), "N32+ GPS MODULE", fill=(220, 230, 245), font=font_bold)
draw.text((120, 368), "UART: 9600 bps 8N1", fill=(160, 190, 230), font=font_small)
# Tira de pines (Header) en la parte inferior del PCB
# 5 pines: VCC, RX, TX, GND, PPS
header_box = [130, 395, 390, 430]
draw_card(header_box, (20, 20, 20), (50, 50, 50), radius=3)
pin_defs = [
("VCC", COLOR_VCC, 160),
("RX", COLOR_RX, 215),
("TX", COLOR_TX, 270),
("GND", COLOR_GND, 325),
("PPS", COLOR_PPS, 370)
]
for name, col, px in pin_defs:
# Pin cuadrado dorado con orificio
draw.rectangle([px-8, 400, px+8, 420], fill=(220, 185, 55), outline=(150, 120, 30))
draw.ellipse([px-4, 406, px+4, 414], fill=(20, 20, 20))
# Texto de serigrafia en PCB
draw.text((px-9, 380), name, fill=col, font=font_chip)
# -------------------------------------------------------------
# 3. DETALLE DE CADA PIN Y CONEXION A LA CYD (COLUMNA DERECHA)
# -------------------------------------------------------------
pins_panel = [500, 85, 960, 480]
draw_card(pins_panel, (14, 18, 30), C_CARD_BORDER, radius=8)
draw.text((515, 95), "DESCRIPCION DE PINES Y DESTINO EN PLACA CYD", fill=C_TEXT_WHITE, font=font_sec)
# Tarjeta Pin 1: VCC
p1_card = [515, 125, 945, 185]
draw_card(p1_card, (26, 32, 48), (200, 50, 50), radius=6)
draw.rectangle([515, 125, 523, 185], fill=COLOR_VCC)
draw.text((535, 132), "PIN 1: VCC (Alimentacion 3.3V a 5.0V DC)", fill=COLOR_VCC, font=font_bold)
draw.text((535, 149), "Entrada de corriente con regulador LDO integrado en la placa GPS.", fill=C_TEXT_MUTED, font=font_small)
draw.text((535, 165), "--> CONECTAR A: Pin 1 del conector P1 de la CYD (Vin 5V USB)", fill=C_TEXT_WHITE, font=font_reg)
# Tarjeta Pin 2: RX
p2_card = [515, 195, 945, 255]
draw_card(p2_card, (26, 32, 48), (200, 120, 20), radius=6)
draw.rectangle([515, 195, 523, 255], fill=COLOR_RX)
draw.text((535, 202), "PIN 2: RX (Entrada Serial UART del GPS - Nivel 3.3V)", fill=COLOR_RX, font=font_bold)
draw.text((535, 219), "Recibe comandos UBX de reposo (Sleep) y despertar (Wake) desde el ESP32.", fill=C_TEXT_MUTED, font=font_small)
draw.text((535, 235), "--> CONECTAR A: Pin 2 del conector P1 de la CYD (GPIO 1 / TX0)", fill=C_TEXT_WHITE, font=font_reg)
# Tarjeta Pin 3: TX
p3_card = [515, 265, 945, 325]
draw_card(p3_card, (26, 32, 48), (30, 160, 90), radius=6)
draw.rectangle([515, 265, 523, 325], fill=COLOR_TX)
draw.text((535, 272), "PIN 3: TX (Salida Serial UART del GPS - Nivel 3.3V)", fill=COLOR_TX, font=font_bold)
draw.text((535, 289), "Transmite sentencias NMEA ($GPRMC, $GPGGA, velocidad, hora satelital).", fill=C_TEXT_MUTED, font=font_small)
draw.text((535, 305), "--> CONECTAR A: Pin 3 del conector P3 de la CYD (GPIO 35 / RX1)", fill=C_TEXT_WHITE, font=font_reg)
# Tarjeta Pin 4: GND
p4_card = [515, 335, 945, 395]
draw_card(p4_card, (26, 32, 48), (30, 90, 180), radius=6)
draw.rectangle([515, 335, 523, 395], fill=COLOR_GND)
draw.text((535, 342), "PIN 4: GND (Masa / Retorno comun)", fill=COLOR_GND, font=font_bold)
draw.text((535, 359), "Retorno de corriente y referencia de nivel logico serial.", fill=C_TEXT_MUTED, font=font_small)
draw.text((535, 375), "--> CONECTAR A: Pin 4 del conector P1 de la CYD (GND)", fill=C_TEXT_WHITE, font=font_reg)
# Tarjeta Pin 5: PPS (Opcional)
p5_card = [515, 405, 945, 465]
draw_card(p5_card, (22, 28, 42), (120, 60, 180), radius=6)
draw.rectangle([515, 405, 523, 465], fill=COLOR_PPS)
draw.text((535, 412), "PIN 5: PPS (Pulse Per Second - Pulso 1 Hz)", fill=COLOR_PPS, font=font_bold)
draw.text((535, 429), "Pulso de sincronismo atómico cada 1 segundo exacto cuando hay fix 3D.", fill=C_TEXT_MUTED, font=font_small)
draw.text((535, 445), "--> NO REQUERIDO: Queda libre / no conectar (sincronismo por NMEA)", fill=(160, 170, 185), font=font_small)
# -------------------------------------------------------------
# 4. ESPECIFICACIONES TECNICAS Y TIPS DE INSTALACION (INFERIOR)
# -------------------------------------------------------------
info_box = [40, 500, 960, 655]
draw_card(info_box, (15, 20, 32), (45, 58, 88), radius=8)
draw.text((55, 510), "ESPECIFICACIONES TECNICAS Y RECOMENDACIONES DE INSTALACION:", fill=C_TEXT_WHITE, font=font_bold)
col1_x = 55
val1_x = 180
draw.text((col1_x, 532), "• Chipset:", fill=C_TEXT_CYAN, font=font_bold)
draw.text((val1_x, 532), "U-blox 7 / NEO-7M (o NEO-6M / N32+) 56 canales.", fill=C_TEXT_WHITE, font=font_small)
draw.text((col1_x, 552), "• Protocolo:", fill=C_TEXT_CYAN, font=font_bold)
draw.text((val1_x, 552), "NMEA 0183 a 9600 bps (8N1 - 8 bits, no paridad, 1 stop).", fill=C_TEXT_WHITE, font=font_small)
draw.text((col1_x, 572), "• Consumo:", fill=C_TEXT_CYAN, font=font_bold)
draw.text((val1_x, 572), "Activo: ~45 mA | Modo Sleep (UBX-RXM-PMREQ): < 0.5 mA.", fill=C_TEXT_WHITE, font=font_small)
draw.text((col1_x, 592), "• Nivel Logico:", fill=C_TEXT_CYAN, font=font_bold)
draw.text((val1_x, 592), "3.3V TTL (compatible directo con pines GPIO del ESP32).", fill=C_TEXT_WHITE, font=font_small)
col2_x = 525
val2_x = 705
draw.text((col2_x, 532), "• Posición de Antena:", fill=(255, 215, 60), font=font_bold)
draw.text((val2_x, 532), "Cara cerámica orientada al cielo.", fill=C_TEXT_WHITE, font=font_small)
draw.text((col2_x, 552), " (En el tablero del vehículo, libre de chapas metálicas o tintes metalizados).", fill=C_TEXT_MUTED, font=font_small)
draw.text((col2_x, 575), "• LED Verde (Fix/PPS):", fill=(80, 235, 130), font=font_bold)
draw.text((val2_x, 575), "Apagado: buscando satélites.", fill=C_TEXT_WHITE, font=font_small)
draw.text((col2_x, 595), " (Destella 1 vez por segundo cuando la señal satelital 3D está fijada).", fill=C_TEXT_MUTED, font=font_small)
draw.text((col1_x, 625), "NOTA: Al programar el ESP32 por USB, si el pin RX del GPS esta conectado a GPIO 1 (P1), desconectar temporalmente el cable amarillo para evitar colisiones.", fill=(255, 180, 80), font=font_small)
# Guardar imagen en hardware/ y en raíz
out_path_1 = "hardware/pinout_gps.png"
out_path_2 = "hardware/gps_pinout.png"
out_path_3 = "pinout_gps.png"
im.save(out_path_1, "PNG")
im.save(out_path_2, "PNG")
im.save(out_path_3, "PNG")
print("Pinout GPS generado exitosamente en:", out_path_1, out_path_2, out_path_3)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

View file

@ -1,4 +1,3 @@
#pragma once
const uint8_t DSEG7_Modern_BoldItalic_80[] PROGMEM = {
0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x50,

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,7 @@
//----------------------------------
extern bool ignitionOn;
extern bool wokeFromSleep;
extern TaskHandle_t btTaskHandle;
void initScreen(bool redrawTFT = true);
void initScreen();
void plotNoData(uint8_t cell);
void touchBeep();
@ -27,9 +26,7 @@ String ByteArraytoString(esp_bd_addr_t bt_address) {
void scanBTdevice() {//scan bluetooth device
setRearLedColor(0, 0, 200);//blue led on
#ifdef SERIAL_DEBUG
Serial.println("Scanning for OBDII Adaptor...");
#endif
Terminal("Scanning for OBDII Adaptor...",0,48,320,191);
btDeviceCount = 0;
// BTScanResults* btDeviceList = BTSerial.getScanResults(); // maybe accessing from different threads!
@ -37,9 +34,7 @@ void scanBTdevice() {//scan bluetooth device
// BTAdvertisedDeviceSet*set = reinterpret_cast<BTAdvertisedDeviceSet*>(pDevice);
// btDeviceList[pDevice->getAddress()] = * set;
String txt = pDevice->toString().c_str();
#ifdef SERIAL_DEBUG
Serial.printf("Found a new device: %s\n", pDevice->toString().c_str());
#endif
deviceName[btDeviceCount] = pDevice->getName().c_str();
deviceAddr[btDeviceCount] = pDevice->getAddress().toString().c_str();
btDeviceCount++;
@ -100,10 +95,7 @@ void scanBTdevice() {//scan bluetooth device
}//while mac address copied to client_addr for connect below code
if (ByteArraytoString(client_addr) != ByteArraytoString(recent_client_addr)) {
pref.putBytes("recent_client",client_addr,sizeof(client_addr));//save new bt addr to pref.
memcpy(recent_client_addr, client_addr, sizeof(client_addr));
#ifdef SERIAL_DEBUG
Serial.println(F("Save a new BT client address"));
#endif
}
break; // Stop scanning once a matching device is found!
} else {
@ -117,16 +109,12 @@ void scanBTdevice() {//scan bluetooth device
if (foundOBD2) {
txt = "Connecting to " + client_name +" - " + ByteArraytoString(client_addr);
Terminal(txt,0,48,320,191);
#ifdef SERIAL_DEBUG
Serial.println(txt);
#endif
BTSerial.connect(client_addr, 0, sec_mask, role);//connect to OBDII adaptor
bool blink = false;
uint8_t try_count = 0;
while(!BTSerial.connected(1000) && (try_count < 10)) {
#ifdef SERIAL_DEBUG
Serial.print(F("."));
#endif
blink =! blink;
if (blink) setRearLedColor(0, 0, 200);
else setRearLedOff();
@ -138,17 +126,13 @@ void scanBTdevice() {//scan bluetooth device
if (try_count >= 10) {
Terminal("Connect failed!",0,48,320,191);
#ifdef SERIAL_DEBUG
Serial.println(F("Connect failed!"));
#endif
BTSerial.disconnect();
setRearLedOff();
foundOBD2 = false;
} else {
Terminal("Connected Successfully!",0,48,320,191);
#ifdef SERIAL_DEBUG
Serial.println(F("Connected Successfully!"));
#endif
prompt = true;
updateRearLedTheme(); // Set rear RGB LED to active theme color
foundOBD2 = true;
@ -156,44 +140,22 @@ void scanBTdevice() {//scan bluetooth device
} else {
Terminal("OBDII Adaptor not found!",0,48,320,191);
#ifdef SERIAL_DEBUG
Serial.println(F("OBDII Adaptor not found!"));
#endif
} //foundOBD2
}//scanBTDevice
//---------------------------
//connect to recent obdII for fast connection, skip scanning
void connectLastOBDII() {
bool hasRecentAddr = false;
for (uint8_t i = 0; i < 6; i++) {
if (recent_client_addr[i] != 0) {
hasRecentAddr = true;
break;
}
}
if (!hasRecentAddr) {
Terminal("Sin escaner OBDII previo.", 0, 48, 320, 191);
#ifdef SERIAL_DEBUG
Serial.println(F("Sin escaner OBDII previo."));
#endif
foundOBD2 = false;
return;
}
setRearLedColor(0, 0, 200);//blue led on
String txt = "Connecting to " + client_name +" - " + ByteArraytoString(recent_client_addr);
Terminal(txt,0,48,320,191);
#ifdef SERIAL_DEBUG
Serial.println(txt);
#endif
BTSerial.connect(recent_client_addr, 0, sec_mask, role);//connect to OBDII adaptor
uint8_t try_count = 0;
bool blink = false;
while(!BTSerial.connected(1000) && (try_count <3)) {
#ifdef SERIAL_DEBUG
Serial.print(F("."));
#endif
blink =! blink;
if (blink) setRearLedColor(0, 0, 200);
else setRearLedOff();
@ -201,17 +163,13 @@ void connectLastOBDII() {
}
if (try_count == 3) {//cannot connect
Terminal("OBDII Adaptor not found!",0,48,320,191);
#ifdef SERIAL_DEBUG
Serial.println(F("OBDII Adaptor not found!"));
#endif
BTSerial.disconnect();
setRearLedOff();
foundOBD2 = false;
} else {
Terminal("Connected Successfully!",0,48,320,191);
#ifdef SERIAL_DEBUG
Serial.println(F("Connected Successfully!"));
#endif
prompt = true;
updateRearLedTheme(); // Set rear RGB LED to active theme color
foundOBD2 = true;
@ -220,12 +178,99 @@ void connectLastOBDII() {
}//connectLasbtOBDII
//---------------------------
//---------------------------
// Resume Bluetooth connection with OBDII scanner upon waking from sleep (en segundo plano, 100% no bloqueante)
// Resume Bluetooth connection with OBDII scanner upon waking from sleep
bool resumeBluetoothConnection() {
foundOBD2 = false;
if (btTaskHandle != NULL) {
xTaskNotifyGive(btTaskHandle);
bool hasRecentAddr = false;
for (uint8_t i = 0; i < 6; i++) {
if (recent_client_addr[i] != 0) {
hasRecentAddr = true;
break;
}
}
if (!hasRecentAddr) {
foundOBD2 = false;
return false;
}
// Draw authentic themed resume card
tft.fillScreen(TFT_BLACK);
tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor());
tft.fillRect(0, 26, 320, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE);
tft.drawCentreString("Conexion Bluetooth", 160, 2, 4);
tft.fillRoundRect(14, 44, 292, 114, 6, getThemeDarkColor());
tft.drawRoundRect(14, 44, 292, 114, 6, getThemeAccentColor());
tft.fillRoundRect(18, 50, 4, 102, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE, getThemeDarkColor());
tft.drawCentreString("Reanudando conexion...", 160, 54, 4);
tft.setTextColor(0xBDD7, getThemeDarkColor());
tft.drawCentreString("Conectando con escaner OBDII", 160, 88, 2);
String macStr = ByteArraytoString(recent_client_addr);
tft.drawCentreString(macStr.c_str(), 160, 108, 2);
// Request Bluetooth connection to OBDII scanner
setRearLedColor(0, 0, 200); // Blue LED indicating connection attempt
BTSerial.disconnect();
delay(50);
BTSerial.connect(recent_client_addr, 0, sec_mask, role);
uint8_t try_count = 0;
bool blink = false;
while (!BTSerial.connected(1000) && (try_count < 4)) {
blink = !blink;
if (blink) setRearLedColor(0, 0, 200);
else setRearLedOff();
String dots = "";
for (uint8_t d = 0; d <= (try_count % 4); d++) dots += ". ";
tft.setTextColor(getThemePrimaryColor(), getThemeDarkColor());
tft.drawCentreString(dots.c_str(), 160, 132, 4);
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty) || digitalRead(SELECTOR_PIN) == LOW) {
break; // Abort on user interaction
}
try_count++;
}
if (BTSerial.connected()) {
tft.setTextColor(TFT_GREEN, getThemeDarkColor());
tft.drawCentreString("[ OK ] Conectado con exito!", 160, 132, 2);
setRearLedColor(0, 255, 0);
delay(200);
// Re-initialize ELM327 protocol and commands for Ford J1850 PWM
sendATCommand("ATE0", 800); // Echo OFF
sendATCommand("ATL0", 800); // Linefeeds OFF
sendATCommand("ATH0", 800); // Headers OFF
sendATCommand("ATAT1", 800); // Normal adaptive timing
sendATCommand("ATSP1", 800); // Ford J1850 PWM (Protocol 1)
String r0100 = sendATCommand("0100", 2500);
ignitionOn = (r0100.indexOf("41 00") != -1);
foundOBD2 = true;
prompt = true;
bt_message = "";
updateRearLedTheme(); // Restore theme color on rear RGB LED
touchBeep();
initScreen();
lastActivityTime = millis();
return true;
} else {
tft.setTextColor(TFT_RED, getThemeDarkColor());
tft.drawCentreString("[!] Escaner no disponible", 160, 132, 2);
BTSerial.disconnect();
setRearLedOff();
foundOBD2 = false;
delay(400);
initScreen();
for (uint8_t c = 0; c < 4; c++) {
plotNoData(c);
}
lastActivityTime = millis();
return false;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,482 +0,0 @@
#pragma once
#include "DSEG7_Modern_BoldItalic_34.h"
#include "DSEG7_Modern_BoldItalic_80.h"
#include "Latin_Sans_Bold_26.h"
#include "rainbow_text.h"
#include "gps_module.h"
#include <Arduino.h>
#include <TFT_eSPI.h>
extern TFT_eSPI tft;
extern uint16_t getThemePrimaryColor();
extern uint16_t getThemeAccentColor();
extern uint16_t getThemeBgColor();
extern bool isRainbowTheme();
extern int8_t currentScreenPage;
extern int8_t fullScreenCell;
// Spanish 3-letter Month abbreviations
static const char *const SPANISH_MONTHS_SHORT[12] = {
"ENE", "FEB", "MAR", "ABR", "MAY", "JUN",
"JUL", "AGO", "SEP", "OCT", "NOV", "DIC"};
// Previous values for flicker-free redrawing
static uint8_t oldClockHour = 255;
static uint8_t oldClockMin = 255;
static uint8_t oldClockSec = 255;
static uint8_t oldClockDay = 255;
static uint8_t oldClockMonth = 255;
static uint16_t oldClockYear = 0;
// Smooth pre-rendered title sprites for Day of Week and Year (100% anti-aliased Latin_Sans_Bold_26 matching Page 1)
static TFT_eSprite sprDayTitle(&tft);
static uint8_t sprDayWday = 255;
inline void updateDayTitleSprite(uint8_t wday) {
if (sprDayWday == wday && sprDayTitle.created()) return;
if (!sprDayTitle.created()) {
sprDayTitle.createSprite(160, 26);
}
if (sprDayTitle.created()) {
uint16_t cardBg = 0x10A2;
sprDayTitle.loadFont(Latin_Sans_Bold_26);
sprDayTitle.setTextPadding(0);
sprDayTitle.fillSprite(cardBg);
sprDayTitle.setTextColor(TFT_WHITE, cardBg);
sprDayTitle.drawString(SPANISH_DAYS[wday], 0, 0, 1);
sprDayTitle.unloadFont();
if (isRainbowTheme()) {
applyRainbowToSprite(sprDayTitle, 4 + 116, 125 + 22, cardBg);
}
sprDayWday = wday;
}
}
static TFT_eSprite sprYearTitle(&tft);
static uint16_t sprYearVal = 0;
static uint16_t sprYearCol = 0;
inline void updateYearTitleSprite(uint16_t year, uint16_t priCol) {
if (sprYearVal == year && sprYearCol == priCol && sprYearTitle.created()) return;
if (!sprYearTitle.created()) {
sprYearTitle.createSprite(80, 26);
}
if (sprYearTitle.created()) {
uint16_t cardBg = 0x10A2;
sprYearTitle.loadFont(Latin_Sans_Bold_26);
sprYearTitle.setTextPadding(0);
sprYearTitle.fillSprite(cardBg);
sprYearTitle.setTextColor(priCol, cardBg);
sprYearTitle.drawString(String(year).c_str(), 0, 0, 1);
sprYearTitle.unloadFont();
if (isRainbowTheme()) {
applyRainbowToSprite(sprYearTitle, 4 + 116, 125 + 64, cardBg);
}
sprYearVal = year;
sprYearCol = priCol;
}
}
inline void initGpsClockTitles() {
updateDayTitleSprite(gpsDayOfWeek % 7);
updateYearTitleSprite(gpsYear, isRainbowTheme() ? (uint16_t)0xFFE0 : getThemeAccentColor());
}
// Draw Full-Width Date Card (Bottom) - Clean, prominent, automotive badge
// layout
inline void drawDateCardContent(int cardX, int cardY, int cardW, int cardH,
uint16_t cardBg, int x_offset = 0) {
if (cardX + cardW <= 0 || cardX >= 320)
return;
uint16_t accCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemeAccentColor();
uint16_t yearCol = isRainbowTheme() ? (uint16_t)0xFFE0 : accCol;
uint16_t cardBorder = 0x2945;
uint8_t wday = gpsDayOfWeek % 7;
uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0;
String mShort = SPANISH_MONTHS_SHORT[mIdx];
String dayStr = (gpsDay < 10) ? "0" + String(gpsDay) : String(gpsDay);
// Left vertical accent bar
tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accCol);
// --- CALENDAR BADGE (Left side: 80x82 px) ---
int bx = cardX + 20;
int by = cardY + 14;
int bw = 80;
int bh = 82;
// Badge body
tft.fillRoundRect(bx, by, bw, bh, 6, 0x18C3);
tft.drawRoundRect(bx, by, bw, bh, 6, cardBorder);
// Badge top header (Month abbreviated in Accent Color)
tft.fillRoundRect(bx, by, bw, 24, 6, accCol);
tft.fillRect(bx, by + 18, bw, 6, accCol); // flatten lower header corners
tft.setTextColor(TFT_BLACK, accCol);
tft.drawCentreString(mShort.c_str(), bx + (bw / 2), by + 4, 2);
// Badge body: Big Day Number in DSEG7 34pt
tft.loadFont(DSEG7_Modern_BoldItalic_34);
tft.setTextColor(TFT_WHITE, 0x18C3);
tft.setTextPadding(0);
tft.drawCentreString(dayStr.c_str(), bx + (bw / 2), by + 34, 1);
tft.unloadFont();
// --- DATE DETAILS (Right side) ---
int tx = cardX + 116;
// Day of week in Latin Sans Bold 26 (Smooth antialiased, White, uppercase matching Page 1)
updateDayTitleSprite(wday);
if (sprDayTitle.created()) {
sprDayTitle.pushSprite(tx, cardY + 22);
} else {
tft.loadFont(Latin_Sans_Bold_26);
tft.setTextPadding(0);
tft.setTextColor(TFT_WHITE, cardBg);
tft.drawString(SPANISH_DAYS[wday], tx, cardY + 22, 1);
tft.unloadFont();
}
// Elegant two-tone horizontal dividing line
tft.drawFastHLine(tx, cardY + 54, 180, cardBorder);
tft.drawFastHLine(tx, cardY + 54, 50, accCol);
// Year in Latin Sans Bold 26 (Smooth antialiased, Accent color)
updateYearTitleSprite(gpsYear, yearCol);
if (sprYearTitle.created()) {
sprYearTitle.pushSprite(tx, cardY + 64);
} else {
tft.loadFont(Latin_Sans_Bold_26);
tft.setTextPadding(0);
tft.setTextColor(yearCol, cardBg);
tft.drawString(String(gpsYear).c_str(), tx, cardY + 64, 1);
tft.unloadFont();
}
}
// Authentic DSEG7 antialiased dot alpha matrix (from
// DSEG7_Modern_BoldItalic_80)
static const uint8_t DSEG7_DOT_ALPHA[11][13] PROGMEM = {
{0x21, 0xEB, 0xEE, 0xED, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEC, 0xEE, 0xD7,
0x0A},
{0x35, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDC,
0x00},
{0x47, 0xFE, 0xFB, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFB, 0xFE, 0xC5,
0x00},
{0x5C, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xB2,
0x00},
{0x71, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x9E,
0x00},
{0x86, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x88,
0x00},
{0x9C, 0xFF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x73,
0x00},
{0xB1, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFB, 0xFF, 0x5E,
0x00},
{0xC5, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFF, 0x49,
0x00},
{0xD7, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0x35,
0x00},
{0xE6, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFC, 0x24,
0x00},
};
// Helper: Draw authentic 7-segment italic colon dots with true anti-aliasing
// matching DSEG7 typography and optional rainbow gradient
template <typename T>
inline void draw7SegDot(T &dst, int x0, int y0, uint16_t color,
uint16_t bgCol, int clip_h = 240, bool rainbow = false) {
dst.startWrite();
for (int dy = 0; dy < 11; dy++) {
int curY = y0 + dy;
if (curY < 0 || curY >= clip_h)
continue;
int fl = 0;
int fxs = 0;
for (int dx = 0; dx < 13; dx++) {
uint8_t a = pgm_read_byte(&(DSEG7_DOT_ALPHA[dy][dx]));
int curX = x0 + dx;
if (a == 0) {
if (fl) {
dst.drawFastHLine(fxs, curY, fl, color);
fl = 0;
}
dst.drawPixel(curX, curY, bgCol);
} else if (a >= 235) {
if (fl == 0)
fxs = curX;
fl++;
} else {
if (fl) {
dst.drawFastHLine(fxs, curY, fl, color);
fl = 0;
}
uint16_t fg = rainbow ? getRainbowScreenColor(curX, curY) : color;
dst.drawPixel(curX, curY, dst.alphaBlend(a, fg, bgCol));
}
}
if (fl) {
if (rainbow) {
for (int i = 0; i < fl; i++) {
dst.drawPixel(fxs + i, curY, getRainbowScreenColor(fxs + i, curY));
}
} else {
dst.drawFastHLine(fxs, curY, fl, color);
}
}
}
dst.endWrite();
}
template <typename T>
inline void draw7SegColon(T &dst, int clkX, int clkY, uint16_t color,
uint16_t bgCol = 0x10A2, int clip_h = 240, bool rainbow = false) {
// Top dot: centered at clkX + 151, font row 19..29 (y = clkY + 30)
draw7SegDot(dst, clkX + 151, clkY + 30, color, bgCol, clip_h, rainbow);
// Bottom dot: centered at clkX + 148, font row 53..63 (y = clkY + 64)
draw7SegDot(dst, clkX + 148, clkY + 64, color, bgCol, clip_h, rainbow);
}
// Synchronize tracking variables without touching TFT display (zero flicker)
inline void syncGpsClockTrackers() {
oldClockHour = gpsHour;
oldClockMin = gpsMinute;
oldClockSec = gpsSecond;
oldClockDay = gpsDay;
oldClockMonth = gpsMonth;
oldClockYear = gpsYear;
}
// Draw full Page 2 (GPS Clock Screen)
inline void drawGpsClockScreen(int x_offset = 0) {
if (x_offset == 0) {
tft.fillScreen(TFT_BLACK);
} else {
tft.fillRect(0, 0, x_offset, 240, TFT_BLACK);
}
uint16_t priCol = getThemePrimaryColor();
uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t cardBg = 0x10A2; // Dark graphite navy
uint16_t cardBorder = 0x2945; // Subtle edge border
bool synced = isGpsSynced();
// 1. TOP BAR (y = 0 to 18)
// Center: 4 Pagination Indicator Dots [○ ○ ● ○] (Page 3 active)
tft.fillCircle(139 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive
tft.fillCircle(153 + x_offset, 2, 2, 0x5AEB); // Page 2 inactive
tft.fillCircle(167 + x_offset, 2, 2, priCol); // Page 3 active
tft.fillCircle(181 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive
// 2. MAIN CLOCK CARD (y = 19 to 121, height = 102, width = 312, x = 4)
int clkX = 4 + x_offset;
int clkY = 19;
int clkW = 312;
int clkH = 102;
tft.fillRoundRect(clkX, clkY, clkW, clkH, 6, cardBg);
tft.drawRoundRect(clkX, clkY, clkW, clkH, 6, cardBorder);
// Left vertical accent bar
tft.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, clkAccCol);
// Digits: Hours HH & Minutes MM with smooth rainbow gradient
uint8_t h10 = (gpsHour / 10) % 10;
uint8_t h1 = gpsHour % 10;
uint8_t m10 = (gpsMinute / 10) % 10;
uint8_t m1 = gpsMinute % 10;
bool rbClk = isRainbowTheme();
drawDseg7Digit80(tft, clkX + 12, clkY + 11, h10, clkAccCol, cardBg, 240, rbClk);
drawDseg7Digit80(tft, clkX + 77, clkY + 11, h1, clkAccCol, cardBg, 240, rbClk);
drawDseg7Digit80(tft, clkX + 170, clkY + 11, m10, clkAccCol, cardBg, 240, rbClk);
drawDseg7Digit80(tft, clkX + 235, clkY + 11, m1, clkAccCol, cardBg, 240, rbClk);
// Blinking Colon Dots between HH and MM (italic 7-segment style with
// anti-aliasing)
bool colOn = (gpsSecond % 2 == 0);
uint16_t colonCol = colOn ? clkAccCol : 0x18C3;
draw7SegColon(tft, clkX, clkY, colonCol, cardBg, 240, colOn && rbClk);
// 3. FULL-WIDTH DATE CARD (y = 125 to 235, height = 110, width = 312, x = 4)
int dateX = 4 + x_offset;
int dateY = 125;
int dateW = 312;
int dateH = 110;
tft.fillRoundRect(dateX, dateY, dateW, dateH, 6, cardBg);
tft.drawRoundRect(dateX, dateY, dateW, dateH, 6, cardBorder);
drawDateCardContent(dateX, dateY, dateW, dateH, cardBg, x_offset);
// Sync state trackers
syncGpsClockTrackers();
}
// 100% Flicker-Free Live Refresh for resting Page 2
inline void updateGpsClockScreen(bool forceRedraw = false) {
if (currentScreenPage != 2 || fullScreenCell != -1)
return;
uint16_t priCol = getThemePrimaryColor();
uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t cardBg = 0x10A2;
int clkX = 4;
int clkY = 19;
// 1. Seconds change: update colon dots only (no seconds text)
if (forceRedraw || gpsSecond != oldClockSec) {
oldClockSec = gpsSecond;
bool colOn = (gpsSecond % 2 == 0);
uint16_t colonCol = colOn ? clkAccCol : 0x18C3;
draw7SegColon(tft, clkX, clkY, colonCol, cardBg, 240, colOn && isRainbowTheme());
}
// 2. Minutes change: update MM digits
if (forceRedraw || gpsMinute != oldClockMin) {
oldClockMin = gpsMinute;
tft.fillRect(clkX + 166, clkY + 10, 142, 82, cardBg);
uint8_t m10 = (gpsMinute / 10) % 10;
uint8_t m1 = gpsMinute % 10;
drawDseg7Digit80(tft, clkX + 170, clkY + 11, m10, clkAccCol, cardBg, 240, isRainbowTheme());
drawDseg7Digit80(tft, clkX + 235, clkY + 11, m1, clkAccCol, cardBg, 240, isRainbowTheme());
}
// 3. Hours change: update HH digits
if (forceRedraw || gpsHour != oldClockHour) {
oldClockHour = gpsHour;
tft.fillRect(clkX + 12, clkY + 10, 136, 82, cardBg);
uint8_t h10 = (gpsHour / 10) % 10;
uint8_t h1 = gpsHour % 10;
drawDseg7Digit80(tft, clkX + 12, clkY + 11, h10, clkAccCol, cardBg, 240, isRainbowTheme());
drawDseg7Digit80(tft, clkX + 77, clkY + 11, h1, clkAccCol, cardBg, 240, isRainbowTheme());
}
// 4. Date changed (midnight rollover): redraw full date card
if (forceRedraw || gpsDay != oldClockDay || gpsMonth != oldClockMonth ||
gpsYear != oldClockYear) {
oldClockDay = gpsDay;
oldClockMonth = gpsMonth;
oldClockYear = gpsYear;
int dateX = 4;
int dateY = 125;
int dateW = 312;
int dateH = 110;
tft.fillRoundRect(dateX, dateY, dateW, dateH, 6, cardBg);
tft.drawRoundRect(dateX, dateY, dateW, dateH, 6, 0x2945);
drawDateCardContent(dateX, dateY, dateW, dateH, cardBg, 0);
}
}
// Render Page 2 into double-buffered sprite slice for slide / fade transitions
inline void renderPage2ToSprite(TFT_eSprite &s, int x_offset, int y_slice,
int slice_h) {
uint16_t priCol = getThemePrimaryColor();
uint16_t accCol = getThemeAccentColor();
uint16_t clkAccCol = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t cardBg = 0x10A2;
uint16_t cardBorder = 0x2945;
// 1. TOP BAR in top slice (pagination dots rendered by caller or top bar)
// 2. MAIN CLOCK CARD (y = 19 to 121)
int clkX = 4 + x_offset;
int clkY = 19 - y_slice;
int clkW = 312;
int clkH = 102;
if (clkX + clkW > 0 && clkX < 320 && clkY + clkH > 0 && clkY < slice_h) {
s.fillRoundRect(clkX, clkY, clkW, clkH, 6, cardBg);
s.drawRoundRect(clkX, clkY, clkW, clkH, 6, cardBorder);
s.fillRoundRect(clkX + 6, clkY + 8, 4, clkH - 16, 2, clkAccCol);
// Colon dots during transition (italic 7-segment style with anti-aliasing)
draw7SegColon(s, clkX, clkY, clkAccCol, cardBg, slice_h, isRainbowTheme());
// Digits: Hours HH (Right aligned to clkX + 142) & Minutes MM (Left aligned
// to clkX + 170)
int digitY = clkY + 11;
if (digitY + 80 > 0 && digitY < slice_h) {
uint8_t h10 = (gpsHour / 10) % 10;
uint8_t h1 = gpsHour % 10;
uint8_t m10 = (gpsMinute / 10) % 10;
uint8_t m1 = gpsMinute % 10;
drawDseg7Digit80(s, clkX + 12, digitY, h10, clkAccCol, cardBg, slice_h, isRainbowTheme());
drawDseg7Digit80(s, clkX + 77, digitY, h1, clkAccCol, cardBg, slice_h, isRainbowTheme());
drawDseg7Digit80(s, clkX + 170, digitY, m10, clkAccCol, cardBg, slice_h, isRainbowTheme());
drawDseg7Digit80(s, clkX + 235, digitY, m1, clkAccCol, cardBg, slice_h, isRainbowTheme());
}
}
// 3. FULL-WIDTH DATE CARD (y = 125 to 235) - Calendar Badge + Details
int dateX = 4 + x_offset;
int dateY = 125 - y_slice;
int dateW = 312;
int dateH = 110;
if (dateX + dateW > 0 && dateX < 320 && dateY + dateH > 0 &&
dateY < slice_h) {
uint16_t dateAccCol = isRainbowTheme() ? (uint16_t)0xFD20 : accCol;
uint16_t yearCol = isRainbowTheme() ? (uint16_t)0xFFE0 : accCol;
s.fillRoundRect(dateX, dateY, dateW, dateH, 6, cardBg);
s.drawRoundRect(dateX, dateY, dateW, dateH, 6, cardBorder);
s.fillRoundRect(dateX + 6, dateY + 8, 4, dateH - 16, 2, dateAccCol);
// Badge container
int bx = dateX + 20;
int by = dateY + 14;
int bw = 80;
int bh = 82;
if (by + bh > 0 && by < slice_h) {
s.fillRoundRect(bx, by, bw, bh, 6, 0x18C3);
s.drawRoundRect(bx, by, bw, bh, 6, cardBorder);
// Header strip
s.fillRoundRect(bx, by, bw, 24, 6, dateAccCol);
s.fillRect(bx, by + 18, bw, 6, dateAccCol);
uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0;
s.setTextColor(TFT_BLACK, dateAccCol);
s.drawCentreString(SPANISH_MONTHS_SHORT[mIdx], bx + (bw / 2), by + 4, 2);
// Badge day number
String dayStr = (gpsDay < 10) ? "0" + String(gpsDay) : String(gpsDay);
s.fontLoaded = true;
s.setTextColor(TFT_WHITE, 0x18C3);
s.setTextPadding(0);
s.drawCentreString(dayStr.c_str(), bx + (bw / 2), by + 34, 1);
s.fontLoaded = false;
}
int tx = dateX + 116;
uint8_t wday = gpsDayOfWeek % 7;
int dwY = dateY + 22;
if (dwY + 26 > 0 && dwY < slice_h) {
updateDayTitleSprite(wday);
if (sprDayTitle.created()) {
sprDayTitle.pushToSprite(&s, tx, dwY);
} else {
s.setTextColor(TFT_WHITE, cardBg);
s.drawString(SPANISH_DAYS[wday], tx, dwY, 4);
}
}
int divY = dateY + 54;
if (divY > 0 && divY < slice_h) {
s.drawFastHLine(tx, divY, 180, cardBorder);
s.drawFastHLine(tx, divY, 50, dateAccCol);
}
int yrY = dateY + 64;
if (yrY + 26 > 0 && yrY < slice_h) {
updateYearTitleSprite(gpsYear, yearCol);
if (sprYearTitle.created()) {
sprYearTitle.pushToSprite(&s, tx, yrY);
} else {
s.setTextColor(yearCol, cardBg);
s.drawString(String(gpsYear).c_str(), tx, yrY, 4);
}
}
}
}

View file

@ -1,160 +0,0 @@
// Generated from accurate vector model matching gps_icon.jpg / gps_icon.svg
// 8-bit Subpixel Antialiased Alpha Maps (0..255)
#pragma once
#include <Arduino.h>
static const uint8_t GPS_BODY_AA_18[324] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 201, 255, 99, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 48, 26, 54, 255, 255, 250, 106, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 171, 255, 238, 115, 216, 211, 55, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 77, 255, 250, 255, 223, 33, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 32, 96, 255, 251, 255, 234, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 106, 240, 183, 165, 255, 243, 94, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 172, 255, 255, 213, 0, 45, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 160, 255, 147, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static const uint8_t GPS_WAVE1_AA_18[324] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31, 18, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 185, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 199, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 254, 137, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 204, 255, 44, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 51, 193, 255, 151, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 29, 227, 225, 229, 255, 255, 152, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 23, 199, 234, 226, 167, 66, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static const uint8_t GPS_WAVE2_AA_18[324] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 96, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 131, 255, 84, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120, 239, 82, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 243, 53, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 216, 229, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 245, 137, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 210, 232, 42, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 182, 248, 112, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 201, 255, 142, 0, 0, 0, 0,
0, 0, 0, 0, 88, 124, 88, 96, 142, 198, 254, 242, 128, 0, 0, 0, 0, 0,
0, 0, 0, 0, 234, 255, 255, 255, 255, 244, 151, 57, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 48, 94, 111, 103, 73, 25, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static const uint8_t GPS_BODY_AA_28[784] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 146, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 213, 255, 255, 66, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 139, 255, 255, 244, 255, 201, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 49, 29, 0, 124, 255, 249, 251, 255, 220, 70, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 120, 235, 255, 242, 135, 38, 241, 255, 255, 155, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 94, 255, 255, 254, 255, 255, 106, 87, 210, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 200, 253, 251, 255, 252, 252, 227, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 218, 253, 253, 255, 254, 254, 238, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 115, 40, 149, 255, 247, 252, 248, 255, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 54, 201, 255, 213, 33, 216, 255, 255, 255, 237, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 126, 250, 255, 243, 255, 133, 16, 130, 169, 137, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 155, 255, 251, 249, 255, 229, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 110, 255, 255, 255, 169, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 181, 234, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static const uint8_t GPS_WAVE1_AA_28[784] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 166, 81, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 221, 255, 245, 20, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 212, 252, 244, 26, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 218, 249, 213, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 249, 243, 158, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 154, 236, 239, 106, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 108, 240, 230, 191, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120, 250, 233, 231, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 79, 76, 51, 68, 116, 193, 249, 232, 255, 139, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 119, 255, 255, 248, 246, 255, 255, 242, 224, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 142, 255, 255, 255, 249, 255, 229, 149, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 123, 149, 157, 140, 103, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static const uint8_t GPS_WAVE2_AA_28[784] PROGMEM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 199, 227, 71, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 93, 255, 255, 179, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 248, 252, 171, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 225, 232, 133, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 228, 225, 106, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 149, 224, 226, 68, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 193, 238, 179, 21, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 109, 243, 227, 119, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 180, 228, 189, 49, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 159, 221, 236, 154, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 139, 243, 233, 192, 34, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 240, 243, 241, 88, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 167, 228, 232, 242, 134, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 74, 144, 225, 244, 229, 235, 121, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 84, 218, 199, 159, 175, 166, 177, 221, 238, 247, 243, 253, 183, 77, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 214, 255, 255, 255, 255, 255, 244, 237, 247, 248, 186, 126, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 107, 238, 242, 231, 255, 245, 224, 196, 175, 102, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 21, 43, 63, 80, 70, 47, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};

View file

@ -1,626 +0,0 @@
#pragma once
#include <Arduino.h>
#include <HardwareSerial.h>
#include <TinyGPSPlus.h>
#include <sys/time.h>
#include <time.h>
#include <TFT_eSPI.h>
#include "gps_icon_aa_data.h"
// Hardware Serial Mapping on ESP32-2432S028 (CYD)
// Conector UART (P1):
// Pin 1: Vin 5V (Alimentacion directa USB / Regulador)
// Pin 2: GPIO 1 (TX0 del ESP32, envia comandos a RX del GPS)
// Pin 3: GPIO 3 (RX0 del ESP32, recibe sentencias NMEA desde TX del GPS)
// Pin 4: GND (Masa comun)
#ifndef GPS_RX_PIN
#define GPS_RX_PIN 3
#endif
#ifndef GPS_TX_PIN
#define GPS_TX_PIN 1
#endif
#define GPS_BAUDRATE 9600
// GPS Hardware Serial en ESP32 UART0 (Hardware Serial nativo de pines 3 y 1)
#define gpsSerial Serial
static TinyGPSPlus gps;
// Timezone offset for Argentina / ART (UTC-3)
#define DEFAULT_TIMEZONE_OFFSET -3
// Spanish Day and Month names
const char *const SPANISH_DAYS[7] = {
"DOMINGO", "LUNES", "MARTES", "MIERCOLES", "JUEVES", "VIERNES", "SABADO"};
const char *const SPANISH_MONTHS[12] = {
"ENERO", "FEBRERO", "MARZO", "ABRIL", "MAYO", "JUNIO",
"JULIO", "AGOSTO", "SEPTIEMBRE", "OCTUBRE", "NOVIEMBRE", "DICIEMBRE"};
const uint8_t DAYS_IN_MONTH_TABLE[12] = {31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31};
// GPS State & Telemetry
static int8_t gpsTimezoneOffset = DEFAULT_TIMEZONE_OFFSET;
static uint8_t gpsHour = 18;
static uint8_t gpsMinute = 0;
static uint8_t gpsSecond = 0;
static uint16_t gpsYear = 2026;
static uint8_t gpsMonth = 9;
static uint8_t gpsDay = 25;
static uint8_t gpsDayOfWeek = 5; // 0=Dom, 1=Lun, ..., 5=Vie, 6=Sab
static bool gpsSynced = false;
static bool gpsHasFix = false;
static uint8_t gpsSats = 0;
static float gpsSpeedKmph = 0.0f;
static float gpsAltitudeM = 0.0f;
static float gpsCourseDeg = 0.0f;
static bool gpsCourseValid = false;
static double originLat = 0.0;
static double originLon = 0.0;
static bool hasOriginLocation = false;
static bool gpsMovedDistanceReached = false;
static const double GPS_MIN_MOVEMENT_METERS = 50.0;
static const float GPS_MAX_HDOP_FOR_COURSE = 2.5f; // Umbral maximo de HDOP para considerar rumbo valido
static const float GPS_MAX_HDOP_FOR_ALT = 2.2f; // Umbral maximo de HDOP para considerar altitud valida
static const uint8_t GPS_MIN_SATS_FOR_ALT = 4; // Minimo 4 satelites para Fix 3D real
static bool gpsAltitudeValid = false;
static float gpsHdop = 99.9f;
static double gpsLatitude = 0.0;
static double gpsLongitude = 0.0;
static bool gpsLocationValid = false;
static uint32_t lastGpsSpeedTime = 0;
static uint32_t lastGpsFixTime = 0;
static bool gpsIsSleeping = false;
static uint32_t lastSecTicker = 0;
static uint32_t lastGpsByteTime = 0;
// GPS sync state
inline bool isGpsSynced() { return gpsSynced && (gpsSats >= 3); }
// Compass direction helpers
inline const char *getCompassDirectionShort(float deg) {
if (deg < 0.0f)
deg += 360.0f;
if (deg >= 360.0f)
deg -= 360.0f;
const char *const dirs[] = {"N", "NE", "E", "SE", "S", "SO", "O", "NO"};
int idx = (int)((deg + 22.5f) / 45.0f) % 8;
return dirs[idx];
}
inline const char *getCompassDirectionFull(float deg) {
if (deg < 0.0f)
deg += 360.0f;
if (deg >= 360.0f)
deg -= 360.0f;
const char *const dirs[] = {"NORTE", "NORESTE", "ESTE", "SURESTE",
"SUR", "SUROESTE", "OESTE", "NOROESTE"};
int idx = (int)((deg + 22.5f) / 45.0f) % 8;
return dirs[idx];
}
// Leap year check
inline bool isLeapYearGps(uint16_t year) {
return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}
// Days in month calculation
inline uint8_t getDaysInMonthGps(uint8_t month, uint16_t year) {
if (month < 1 || month > 12)
return 30;
if (month == 2 && isLeapYearGps(year))
return 29;
return DAYS_IN_MONTH_TABLE[month - 1];
}
// Sakamoto's algorithm for day of week (0 = Sunday, 1 = Monday, ..., 6 =
// Saturday)
inline uint8_t calcDayOfWeekGps(int y, int m, int d) {
static const int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
if (m < 3)
y -= 1;
return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
}
// Configure GPS module (U-blox UBX + MediaTek PMTK + Casic PCAS)
inline void configureGpsModule() {
// A. U-blox UBX-CFG-NAV5: Dynamic platform model = 4 (Automotive / Coche)
const uint8_t ubxNav5Auto[] = {
0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x04, 0x03,
0x00, 0x00, 0x00, 0x00, 0x10, 0x27, 0x00, 0x00, 0x05, 0x00,
0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00, 0x2C, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x16, 0xDC};
gpsSerial.write(ubxNav5Auto, sizeof(ubxNav5Auto));
gpsSerial.flush();
delay(15);
// B. Standard NMEA sentences configuration (GGA, RMC, GSA, GSV, VTG)
// MediaTek / MT3339 / MT3333
gpsSerial.println("$PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*28");
// Zhongke Micro CASIC / ATGM336H: Activar todas las constelaciones y sentencias
gpsSerial.println("$PCAS03,1,1,1,1,1,1,0,0,0,0,,,0,0*03");
gpsSerial.flush();
}
extern Preferences pref;
static const uint32_t CANDIDATE_BAUDS[4] = {38400, 115200, 9600, 57600};
static const char *const CANDIDATE_LABELS[4] = {
"BUSCANDO: 38400", "BUSCANDO: 115K", "BUSCANDO: 9600", "BUSCANDO: 57.6K"};
static uint32_t activeGpsBaud = 38400;
static bool gpsUartLocked = false;
static uint8_t gpsBaudScanIdx = 0;
static uint32_t lastBaudSwitchTime = 0;
static uint32_t lastValidSentenceTime = 0;
static char gpsDiagLabel[28] = "EN ESPERA DE SENAL";
// Initialize GPS Hardware Serial UART0 (conector UART: Pin 3 GPIO 3, Pin 2 GPIO 1)
inline void setupGPS() {
gpsSerial.end();
delay(15);
gpsSerial.setRxBufferSize(512); // Must be before begin()!
// Cargar baudrate guardado en memoria no volatil (o arrancar en 38400 por defecto)
activeGpsBaud = pref.getUInt("gps_baud", 38400);
if (activeGpsBaud != 9600 && activeGpsBaud != 38400 && activeGpsBaud != 115200 && activeGpsBaud != 57600) {
activeGpsBaud = 38400;
}
// Ubicar indice correspondiente
gpsBaudScanIdx = 0;
for (uint8_t i = 0; i < 4; i++) {
if (CANDIDATE_BAUDS[i] == activeGpsBaud) {
gpsBaudScanIdx = i;
break;
}
}
gpsSerial.begin(activeGpsBaud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
delay(25);
while (gpsSerial.available() > 0) {
gpsSerial.read();
}
lastSecTicker = millis();
lastGpsByteTime = millis();
lastBaudSwitchTime = millis();
lastValidSentenceTime = 0;
gpsUartLocked = false;
snprintf(gpsDiagLabel, sizeof(gpsDiagLabel), "%s", CANDIDATE_LABELS[gpsBaudScanIdx]);
// Check if ESP32 internal RTC already has valid time
time_t now = time(NULL);
if (now > 1600000000) {
struct tm *loc = gmtime(&now);
if (loc) {
gpsHour = loc->tm_hour;
gpsMinute = loc->tm_min;
gpsSecond = loc->tm_sec;
gpsDay = loc->tm_mday;
gpsMonth = loc->tm_mon + 1;
gpsYear = loc->tm_year + 1900;
gpsDayOfWeek = loc->tm_wday;
gpsSynced = true;
}
}
#ifdef SERIAL_DEBUG
Serial.printf(
"[GPS] HardwareSerial UART0 inicializado en RX:%d, TX:%d a %d bps\n",
GPS_RX_PIN, GPS_TX_PIN, activeGpsBaud);
#endif
}
// Put GPS module to sleep (UBX backup mode + NMEA Standby for MediaTek &
// Casic/ATGM336)
inline void gpsSleep() {
if (gpsIsSleeping)
return;
// 1. NMEA Standby command for MediaTek (MTK3339 / MT3333 / Adafruit GPS)
gpsSerial.println("$PMTK161,0*28");
// 2. CASIC Standby command for ATGM336H / Zhongke Micro
gpsSerial.println("$PCAS12,1*5A");
gpsSerial.flush();
delay(10);
// 3. UBX-RXM-PMREQ command: put U-blox GPS to sleep (Backup mode)
// IMPORTANTE: Debe enviarse al final de todo ya que U-blox tiene "Wake-on-UART"
// y cualquier carácter o flanco posterior en la línea RX lo despierta inmediatamente.
const uint8_t ubxSleep[] = {0xB5, 0x62, 0x02, 0x41, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4D, 0x3B};
gpsSerial.write(ubxSleep, sizeof(ubxSleep));
gpsSerial.flush();
gpsIsSleeping = true;
gpsLocationValid = false;
gpsSpeedKmph = 0.0f;
}
// Wake on UART: send burst of bytes to awaken U-blox / MTK / ATGM336
inline void gpsWake() {
if (!gpsIsSleeping)
return;
// 1. Wake U-blox: burst of dummy bytes to trigger Wake-on-UART
for (uint8_t i = 0; i < 32; i++) {
gpsSerial.write(0xFF);
}
// 2. Wake MediaTek / NMEA: send CR/LF dummy string
gpsSerial.println();
gpsSerial.println("$PMTK000*32");
gpsSerial.flush();
// Clear serial RX buffer so corrupted burst bytes don't confuse TinyGPSPlus
delay(30);
while (gpsSerial.available() > 0) {
gpsSerial.read();
}
gpsIsSleeping = false;
lastSecTicker = millis();
// Restore atomic time from ESP32 internal RTC immediately upon wake
time_t now = time(NULL);
if (now > 1600000000) {
struct tm *loc = gmtime(&now);
if (loc) {
gpsHour = loc->tm_hour;
gpsMinute = loc->tm_min;
gpsSecond = loc->tm_sec;
gpsDay = loc->tm_mday;
gpsMonth = loc->tm_mon + 1;
gpsYear = loc->tm_year + 1900;
gpsDayOfWeek = loc->tm_wday;
}
}
#ifdef SERIAL_DEBUG
Serial.println(F(">>> [GPS] Modulo GPS despertado (Wake-up)."));
#endif
}
// Process incoming NMEA stream from GPS
inline void processGPS() {
if (gpsIsSleeping)
return;
// 1. Process incoming bytes directly
while (gpsSerial.available() > 0) {
char c = (char)gpsSerial.read();
gps.encode(c);
lastGpsByteTime = millis();
}
// 2. Lock check: Verified valid NMEA sentence with valid checksum!
if (gps.passedChecksum() >= 1) {
lastValidSentenceTime = millis();
if (!gpsUartLocked) {
gpsUartLocked = true;
activeGpsBaud = CANDIDATE_BAUDS[gpsBaudScanIdx];
pref.putUInt("gps_baud", activeGpsBaud); // Guardar velocidad exacta en NVRAM!
snprintf(gpsDiagLabel, sizeof(gpsDiagLabel), "FIJADO: %lu BPS", (unsigned long)activeGpsBaud);
configureGpsModule();
}
}
// 3. Auto-Scan across bauds only if not yet locked
if (!gpsUartLocked) {
if (millis() - lastBaudSwitchTime > 1800) {
lastBaudSwitchTime = millis();
gpsBaudScanIdx = (gpsBaudScanIdx + 1) % 4;
activeGpsBaud = CANDIDATE_BAUDS[gpsBaudScanIdx];
gpsSerial.end();
delay(15);
gpsSerial.setRxBufferSize(512);
gpsSerial.begin(activeGpsBaud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
while (gpsSerial.available() > 0) {
gpsSerial.read();
}
gps = TinyGPSPlus();
snprintf(gpsDiagLabel, sizeof(gpsDiagLabel), "%s", CANDIDATE_LABELS[gpsBaudScanIdx]);
}
} else {
// Si pasan mas de 10 segundos sin recibir NINGUNA trama valida, re-escanear
if (millis() - lastValidSentenceTime > 10000) {
gpsUartLocked = false;
lastBaudSwitchTime = millis();
}
}
// Update Satellites count
if (gps.satellites.isValid()) {
gpsSats = gps.satellites.value();
}
// Update HDOP & Altitude
if (gps.hdop.isValid()) {
gpsHdop = gps.hdop.hdop();
}
if (gps.altitude.isValid() && (gps.altitude.age() < 3000) &&
(gpsSats >= GPS_MIN_SATS_FOR_ALT) && (gpsHdop <= GPS_MAX_HDOP_FOR_ALT)) {
float rawAlt = (float)gps.altitude.meters();
if (!gpsAltitudeValid || gpsAltitudeM == 0.0f) {
gpsAltitudeM = rawAlt;
} else {
// Filtro exponencial suave (EMA 80/20) para amortiguar ruido vertical
gpsAltitudeM = (gpsAltitudeM * 0.80f) + (rawAlt * 0.20f);
}
gpsAltitudeValid = true;
} else {
if (gpsHdop > GPS_MAX_HDOP_FOR_ALT || gpsSats < GPS_MIN_SATS_FOR_ALT || !gps.altitude.isValid()) {
gpsAltitudeValid = false;
}
}
// Update Coordinates and track initial position for movement threshold (>= 50m)
if (gps.location.isValid()) {
gpsLatitude = gps.location.lat();
gpsLongitude = gps.location.lng();
gpsLocationValid = (gps.location.age() < 4000);
// Fijar punto de origen la primera vez que se sincroniza posicion
if (!hasOriginLocation && isGpsSynced()) {
originLat = gpsLatitude;
originLon = gpsLongitude;
hasOriginLocation = true;
}
} else {
gpsLocationValid = false;
}
// Update Speed & Course from GPS
if (gps.speed.isValid() && gps.speed.age() < 2500) {
float rawSpd = (float)gps.speed.kmph();
// Filtro de ruido en reposo (Deadband automotriz):
// Si la velocidad es menor a 4.5 km/h, se considera vehiculo detenido (0 km/h)
// para evitar fluctuaciones Doppler y ruido de antena cuando el auto esta parado.
if (rawSpd < 4.5f) {
gpsSpeedKmph = 0.0f;
// CLAVE: Mientras este quieto y aun no haya superado el umbral de movimiento,
// anclamos el punto de origen a la posicion actual para que la deriva de la antena
// en reposo NUNCA acumule distancia ni active el rumbo.
if (!gpsMovedDistanceReached && gpsLocationValid) {
originLat = gpsLatitude;
originLon = gpsLongitude;
}
} else {
// Filtro exponencial suave (EMA) para suavizar saltos y transiciones:
if (gpsSpeedKmph < 0.1f) {
gpsSpeedKmph = rawSpd; // Respuesta instantanea al arrancar
} else {
gpsSpeedKmph = (gpsSpeedKmph * 0.35f) + (rawSpd * 0.65f); // 65% lectura actual, 35% anterior
}
// Si el auto esta rodando y la señal es precisa (HDOP bajo), comprobar si ya supero los 50 metros desde el origen
if (hasOriginLocation && !gpsMovedDistanceReached && gpsLocationValid && (gpsHdop <= GPS_MAX_HDOP_FOR_COURSE)) {
double distFromOrigin = TinyGPSPlus::distanceBetween(originLat, originLon, gpsLatitude, gpsLongitude);
if (distFromOrigin >= GPS_MIN_MOVEMENT_METERS) {
gpsMovedDistanceReached = true;
}
}
}
lastGpsSpeedTime = millis();
if (gpsSats >= 3) {
gpsHasFix = true;
}
// Actualizacion de Rumbo / Brujula (Heading / Course Over Ground):
// REQUISITO ESTRICTO:
// 1. Si no se recorrieron al menos 50 metros en movimiento real, NO muestra nada ("---").
// 2. Si el HDOP es alto (> 2.5), NO calcula rumbo para evitar derivas por antena o precision degradada.
// 3. Una vez superados los 50 metros y rodando (>= 4.5 km/h) con buen HDOP, calcula y muestra el rumbo real.
// 4. Al detenerse (habiendo recorrido ya los 50 metros), CONGELA el rumbo para que no oscile en reposo.
if (gpsMovedDistanceReached) {
if (rawSpd >= 4.5f && gps.course.isValid() && (gpsHdop <= GPS_MAX_HDOP_FOR_COURSE)) {
float newCourse = (float)gps.course.deg();
if (!gpsCourseValid) {
gpsCourseDeg = newCourse;
gpsCourseValid = true;
} else {
// Suavizado angular circular (maneja el cruce 0 <-> 360 grados sin tirones)
float diff = newCourse - gpsCourseDeg;
if (diff > 180.0f) diff -= 360.0f;
if (diff < -180.0f) diff += 360.0f;
gpsCourseDeg += diff * 0.45f;
if (gpsCourseDeg < 0.0f) gpsCourseDeg += 360.0f;
if (gpsCourseDeg >= 360.0f) gpsCourseDeg -= 360.0f;
}
}
}
}
// Update Atomic Time and Date with Timezone Offset
if (gps.time.isValid() && gps.date.isValid() &&
(gps.time.isUpdated() || gps.date.isUpdated())) {
struct tm t;
memset(&t, 0, sizeof(t));
t.tm_year = gps.date.year() - 1900;
t.tm_mon = gps.date.month() - 1;
t.tm_mday = gps.date.day();
t.tm_hour = gps.time.hour();
t.tm_min = gps.time.minute();
t.tm_sec = gps.time.second();
t.tm_isdst = 0;
time_t utc = mktime(&t);
time_t local = utc + ((int32_t)gpsTimezoneOffset * 3600);
struct tm *loc = gmtime(&local);
if (loc) {
gpsHour = loc->tm_hour;
gpsMinute = loc->tm_min;
gpsSecond = loc->tm_sec;
gpsDay = loc->tm_mday;
gpsMonth = loc->tm_mon + 1;
gpsYear = loc->tm_year + 1900;
gpsDayOfWeek = loc->tm_wday; // 0=Dom, 1=Lun, ..., 6=Sab
gpsSynced = true;
lastGpsFixTime = millis();
lastSecTicker = millis();
// Synchronize ESP32 internal system RTC clock (POSIX time)
struct timeval tv = {.tv_sec = local, .tv_usec = 0};
settimeofday(&tv, NULL);
}
}
// Smooth internal 1-second ticker (keeps clock ticking smoothly even if GPS
// drops a frame or indoor)
if (millis() - lastSecTicker >= 1000) {
uint32_t elapsed = (millis() - lastSecTicker) / 1000;
lastSecTicker += elapsed * 1000;
if (!gps.time.isUpdated()) {
gpsSecond += elapsed;
if (gpsSecond >= 60) {
uint8_t addM = gpsSecond / 60;
gpsSecond %= 60;
gpsMinute += addM;
if (gpsMinute >= 60) {
uint8_t addH = gpsMinute / 60;
gpsMinute %= 60;
gpsHour = (gpsHour + addH) % 24;
if (gpsHour == 0 && addH > 0) {
// Day rollover
gpsDay++;
uint8_t maxD = getDaysInMonthGps(gpsMonth, gpsYear);
if (gpsDay > maxD) {
gpsDay = 1;
gpsMonth++;
if (gpsMonth > 12) {
gpsMonth = 1;
gpsYear++;
}
}
gpsDayOfWeek = (gpsDayOfWeek + 1) % 7;
}
}
}
}
}
}
// True when GPS has satellite lock and is actively providing vehicle telemetry
inline bool isGpsSpeedValid() {
return isGpsSynced() && (millis() - lastGpsByteTime < 3500);
}
// Get current GPS speed in km/h
inline float getGpsSpeed() { return gpsSpeedKmph; }
// Course / Heading valid state (only valid after moving >= 50 meters and with good HDOP)
inline bool isGpsCourseValid() {
return gpsCourseValid && gpsMovedDistanceReached && (gpsHdop <= GPS_MAX_HDOP_FOR_COURSE);
}
// Altitude valid state (requires true 3D Fix: >= 4 sats and HDOP <= 2.2)
inline bool isGpsAltitudeValid() {
return isGpsSynced() && gpsAltitudeValid && (gpsSats >= GPS_MIN_SATS_FOR_ALT) && (gpsHdop <= GPS_MAX_HDOP_FOR_ALT);
}
// =============================================================================
// GPS DIAGNOSTICS & TELEMETRY
// =============================================================================
inline uint32_t getGpsCharsProcessed() { return gps.charsProcessed(); }
inline uint32_t getGpsPassedChecksum() { return gps.passedChecksum(); }
inline uint32_t getGpsFailedChecksum() { return gps.failedChecksum(); }
inline bool isGpsSerialReceiving() {
return (gps.charsProcessed() > 0 && (millis() - lastGpsByteTime < 2500));
}
inline bool isGpsUartLocked() { return gpsUartLocked; }
inline const char *getGpsDiagMessage() {
if (gpsIsSleeping)
return "MODO REPOSO";
if (!gpsUartLocked) {
return gpsDiagLabel;
}
if (millis() - lastGpsByteTime > 4000) {
return "SIN DATOS RX";
}
if (isGpsSynced()) {
return "PRECISION OPTIMA";
}
if (gpsSats > 0) {
return "SINTONIZANDO SATS";
}
return gpsDiagLabel;
}
// =============================================================================
// ANIMATED SATELLITE ICON (Fiel y Exacto a gps_icon.jpg)
// 3 Estados en Búsqueda:
// 0: Satélite SIN ondas
// 1: Satélite con PRIMERA onda
// 2: Satélite con PRIMERA y SEGUNDA onda
// -> vuelve a 0 (de nuevo satélite sin ondas)
// Cuando fija satélites (gpsSats >= 3): queda fijo con ambas ondas encendidas
// =============================================================================
inline uint8_t getGpsWaveAnimState() {
if (isGpsSynced()) {
return 2; // Satélite con las dos ondas encendidas fijas
}
// Ciclo continuo de 3 pasos cada 350ms: 0 -> 1 -> 2 -> 0 -> 1 -> 2 ...
return (millis() / 350) % 3;
}
// Icono estandar antialiased (18x18 px) para Cuadrante 1 Velocidad, Reloj GPS y Barra Superior
template <typename T>
inline void drawGpsSatelliteIcon(T &d, int x, int y, uint16_t bodyColor,
uint16_t waveColor, uint16_t bgColor,
uint8_t waveState) {
int idx = 0;
for (int row = 0; row < 18; row++) {
for (int col = 0; col < 18; col++, idx++) {
uint8_t a_body = pgm_read_byte(&GPS_BODY_AA_18[idx]);
uint8_t a_w1 = (waveState >= 1) ? pgm_read_byte(&GPS_WAVE1_AA_18[idx]) : 0;
uint8_t a_w2 = (waveState >= 2) ? pgm_read_byte(&GPS_WAVE2_AA_18[idx]) : 0;
uint8_t a_w_max = pgm_read_byte(&GPS_WAVE1_AA_18[idx]) | pgm_read_byte(&GPS_WAVE2_AA_18[idx]);
if (a_body < 10 && a_w_max < 10) continue;
uint16_t c = bgColor;
uint8_t a_w = (a_w1 > a_w2) ? a_w1 : a_w2;
if (a_w >= 10) {
c = (a_w >= 245) ? waveColor : tft.alphaBlend(a_w, waveColor, bgColor);
}
if (a_body >= 10) {
c = (a_body >= 245) ? bodyColor : tft.alphaBlend(a_body, bodyColor, c);
}
d.drawPixel(x + col, y + row, c);
}
}
}
// Icono grande antialiased (28x28 px) para Tarjeta 2 de Telemetria en gps_nav.h
template <typename T>
inline void drawLargeGpsSatelliteIcon(T &d, int x, int y, uint16_t bodyColor,
uint16_t waveColor, uint16_t bgColor,
uint8_t waveState) {
int idx = 0;
for (int row = 0; row < 28; row++) {
for (int col = 0; col < 28; col++, idx++) {
uint8_t a_body = pgm_read_byte(&GPS_BODY_AA_28[idx]);
uint8_t a_w1 = (waveState >= 1) ? pgm_read_byte(&GPS_WAVE1_AA_28[idx]) : 0;
uint8_t a_w2 = (waveState >= 2) ? pgm_read_byte(&GPS_WAVE2_AA_28[idx]) : 0;
uint8_t a_w_max = pgm_read_byte(&GPS_WAVE1_AA_28[idx]) | pgm_read_byte(&GPS_WAVE2_AA_28[idx]);
if (a_body < 10 && a_w_max < 10) continue;
uint16_t c = bgColor;
uint8_t a_w = (a_w1 > a_w2) ? a_w1 : a_w2;
if (a_w >= 10) {
c = (a_w >= 245) ? waveColor : tft.alphaBlend(a_w, waveColor, bgColor);
}
if (a_body >= 10) {
c = (a_body >= 245) ? bodyColor : tft.alphaBlend(a_body, bodyColor, c);
}
d.drawPixel(x + col, y + row, c);
}
}
}

View file

@ -1,475 +0,0 @@
#pragma once
#include <Arduino.h>
#include <TFT_eSPI.h>
#include "gps_module.h"
#include "DSEG7_Modern_BoldItalic_34.h"
#include "rainbow_text.h"
extern TFT_eSPI tft;
extern uint16_t getThemePrimaryColor();
extern uint16_t getThemeAccentColor();
extern uint16_t getThemeBgColor();
extern bool isRainbowTheme();
extern int8_t currentScreenPage;
extern int8_t fullScreenCell;
// Previous values for flicker-free redrawing of Page 3
static float oldNavAlt = -999.0f;
static float oldNavCourse = -999.0f;
static uint8_t oldNavSats = 255;
static float oldNavHdop = -999.0f;
static double oldNavLat = 0.0;
static double oldNavLon = 0.0;
static bool oldNavFix = false;
static int lastNavAltRound = -999;
static bool lastNavAltValid = false;
static int lastNavCrsRound = -999;
static bool lastNavHasCourse = false;
static const char* lastNavCardDir = "";
static uint8_t lastNavCardSats = 255;
static bool lastNavSatsSync = false;
static bool lastNavUartLocked = false;
static float lastNavCardHdop = -999.0f;
static char lastNavCardDiag[32] = "";
static bool lastNavDiagSynced = false;
static uint8_t oldNavWaveState = 255;
static bool oldNavSynced = false;
static uint32_t lastNavPageEnterTime = 0;
// Synchronize tracking variables without touching TFT display (zero flicker)
inline void syncGpsNavTrackers() {
lastNavPageEnterTime = millis();
bool synced = isGpsSynced();
bool altValid = isGpsAltitudeValid();
lastNavAltRound = altValid ? (int)round(gpsAltitudeM) : -999;
lastNavAltValid = altValid;
oldNavAlt = gpsAltitudeM;
bool hasCourse = synced && isGpsCourseValid();
lastNavCrsRound = hasCourse ? (int)round(gpsCourseDeg) : -999;
lastNavHasCourse = hasCourse;
oldNavCourse = gpsCourseDeg;
lastNavCardDir = hasCourse ? getCompassDirectionFull(gpsCourseDeg) : "---";
lastNavCardSats = gpsSats;
lastNavSatsSync = synced;
lastNavUartLocked = isGpsUartLocked();
oldNavSats = gpsSats;
lastNavCardHdop = gpsHdop;
oldNavHdop = gpsHdop;
strncpy(lastNavCardDiag, getGpsDiagMessage(), sizeof(lastNavCardDiag) - 1);
lastNavCardDiag[sizeof(lastNavCardDiag) - 1] = '\0';
lastNavDiagSynced = synced;
oldNavWaveState = getGpsWaveAnimState();
oldNavSynced = synced;
oldNavLat = gpsLatitude;
oldNavLon = gpsLongitude;
oldNavFix = synced;
}
// Large animated satellite icon faithful to gps_icon.jpg for the Navigation Dashboard
inline void drawLargeSatelliteIcon(int x, int y, uint16_t color, uint8_t waveState = 2, uint16_t bgColor = 0x10A2) {
drawLargeGpsSatelliteIcon(tft, x, y, TFT_WHITE, color, bgColor, waveState);
}
// Large animated satellite icon for double-buffered sprite slice
inline void drawLargeSatelliteIconSprite(TFT_eSprite &s, int x, int y, uint16_t color, uint8_t waveState = 2, uint16_t bgColor = 0x10A2) {
drawLargeGpsSatelliteIcon(s, x, y, TFT_WHITE, color, bgColor, waveState);
}
// Draw full Page 3 (GPS Navigation & Telemetry Screen: Altitud, Rumbo, Satélites, Coordenadas)
inline void drawGpsNavScreen(int x_offset = 0) {
if (x_offset == 0) {
tft.fillScreen(TFT_BLACK);
} else {
tft.fillRect(0, 0, x_offset, 240, TFT_BLACK);
}
uint16_t priCol = getThemePrimaryColor();
uint16_t accCol = getThemeAccentColor();
uint16_t c0Col = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t c1Col = isRainbowTheme() ? (uint16_t)0x07E0 : accCol;
uint16_t c2Col = isRainbowTheme() ? (uint16_t)0xFFE0 : priCol;
uint16_t c3Col = isRainbowTheme() ? (uint16_t)0xFD20 : accCol;
uint16_t cardBg = 0x10A2; // Dark graphite navy
uint16_t cardBorder = 0x2945; // Subtle edge border
bool synced = isGpsSynced();
// 1. TOP BAR (y = 0 to 18)
// Center: 4 Pagination Indicator Dots [○ ○ ○ ●] (Page 4 active)
tft.fillCircle(139 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive
tft.fillCircle(153 + x_offset, 2, 2, 0x5AEB); // Page 2 inactive
tft.fillCircle(167 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive
tft.fillCircle(181 + x_offset, 2, 2, priCol); // Page 4 active
// 2. CARD 0 (Top Left): ALTITUD (x = 4, y = 19, w = 152, h = 102)
int c0X = 4 + x_offset;
int c0Y = 19;
int cW = 152;
int cH1 = 102;
tft.fillRoundRect(c0X, c0Y, cW, cH1, 6, cardBg);
tft.drawRoundRect(c0X, c0Y, cW, cH1, 6, cardBorder);
tft.fillRoundRect(c0X + 6, c0Y + 8, 4, cH1 - 16, 2, c0Col);
tft.setTextColor(0x8410, cardBg);
tft.drawString("ALTITUD", c0X + 16, c0Y + 8, 2);
tft.setTextColor(0x9CD3, cardBg);
tft.drawString("MSNM", c0X + 96, c0Y + 8, 2);
bool altValid = isGpsAltitudeValid();
String altStr = altValid ? String((int)round(gpsAltitudeM)) : "---";
drawDseg7RightString34(tft, altStr.c_str(), c0X + 140, c0Y + 28, c0Col, cardBg, 0, 240, isRainbowTheme());
// 3. CARD 1 (Top Right): RUMBO / BRÚJULA (x = 164, y = 19, w = 152, h = 102)
int c1X = 164 + x_offset;
int c1Y = 19;
tft.fillRoundRect(c1X, c1Y, cW, cH1, 6, cardBg);
tft.drawRoundRect(c1X, c1Y, cW, cH1, 6, cardBorder);
tft.fillRoundRect(c1X + 6, c1Y + 8, 4, cH1 - 16, 2, c1Col);
tft.setTextColor(0x8410, cardBg);
tft.drawString("RUMBO / BRUJULA", c1X + 16, c1Y + 8, 2);
bool hasCourse = synced && isGpsCourseValid();
String crsStr = hasCourse ? String((int)round(gpsCourseDeg)) : "---";
drawDseg7RightString34(tft, crsStr.c_str(), c1X + 130, c1Y + 28, c1Col, cardBg, 0, 240, isRainbowTheme());
if (hasCourse) {
tft.drawCircle(c1X + 137, c1Y + 33, 3, c1Col);
tft.drawCircle(c1X + 137, c1Y + 33, 2, c1Col);
} else {
tft.fillCircle(c1X + 137, c1Y + 33, 4, cardBg);
}
const char* cardDir = hasCourse ? getCompassDirectionFull(gpsCourseDeg) : "---";
tft.setTextColor(TFT_WHITE, cardBg);
tft.drawString(cardDir, c1X + 16, c1Y + 74, 2);
// 4. CARD 2 (Bottom Left): SATÉLITES GPS EN GRANDE (x = 4, y = 125, w = 152, h = 110)
int c2X = 4 + x_offset;
int c2Y = 125;
int cH2 = 110;
tft.fillRoundRect(c2X, c2Y, cW, cH2, 6, cardBg);
tft.drawRoundRect(c2X, c2Y, cW, cH2, 6, cardBorder);
tft.fillRoundRect(c2X + 6, c2Y + 8, 4, cH2 - 16, 2, c2Col);
tft.setTextColor(0x8410, cardBg);
tft.drawString("SATELITES GPS", c2X + 16, c2Y + 8, 2);
// Large Satellite Icon (animated body + waves)
uint8_t waveState = getGpsWaveAnimState();
drawLargeSatelliteIcon(c2X + 16, c2Y + 32, c2Col, waveState, cardBg);
// Large Satellites Count (DSEG7 34pt): Shows "---" while scanning/probing UART, "0" or count when locked
String satStr = isGpsUartLocked() ? String(gpsSats) : "---";
drawDseg7RightString34(tft, satStr.c_str(), c2X + 140, c2Y + 26, c2Col, cardBg, 0, 240, isRainbowTheme());
// Telemetry details: HDOP precision & live diagnostic status
String hdopStr = "HDOP: " + (synced ? String(gpsHdop, 1) : "---");
tft.setTextColor(TFT_WHITE, cardBg);
tft.drawString(hdopStr.c_str(), c2X + 16, c2Y + 68, 2);
tft.setTextColor(synced ? 0x9CD3 : priCol, cardBg);
tft.drawString(getGpsDiagMessage(), c2X + 16, c2Y + 90, 1);
// 5. CARD 3 (Bottom Right): COORDENADAS GPS (x = 164, y = 125, w = 152, h = 110)
int c3X = 164 + x_offset;
int c3Y = 125;
tft.fillRoundRect(c3X, c3Y, cW, cH2, 6, cardBg);
tft.drawRoundRect(c3X, c3Y, cW, cH2, 6, cardBorder);
tft.fillRoundRect(c3X + 6, c3Y + 8, 4, cH2 - 16, 2, c3Col);
tft.setTextColor(0x8410, cardBg);
tft.drawString("COORDENADAS", c3X + 16, c3Y + 8, 2);
tft.setTextColor(0x8410, cardBg);
tft.drawString("LAT", c3X + 16, c3Y + 26, 1);
String latStr = gpsLocationValid ? (String(gpsLatitude, 5) + "°") : "--.-----°";
tft.setTextColor(c3Col, cardBg);
tft.drawString(latStr.c_str(), c3X + 16, c3Y + 38, 2);
tft.setTextColor(0x8410, cardBg);
tft.drawString("LON", c3X + 16, c3Y + 64, 1);
String lonStr = gpsLocationValid ? (String(gpsLongitude, 5) + "°") : "--.-----°";
tft.setTextColor(c3Col, cardBg);
tft.drawString(lonStr.c_str(), c3X + 16, c3Y + 76, 2);
// Sync state trackers
syncGpsNavTrackers();
}
// 100% Flicker-Free Live Refresh for resting Page 3 (GPS Navigation & Telemetry)
inline void updateGpsNavScreen(bool forceRedraw = false) {
if (currentScreenPage != 3 || fullScreenCell != -1) return;
// Post-transition stabilization: during the first 400ms after landing on Page 3,
// do not wipe or repaint cards over SPI. Any incoming serial bytes from GPS
// are safely absorbed into tracking variables so there is ZERO blackout/blink!
if (!forceRedraw && (millis() - lastNavPageEnterTime < 400)) {
syncGpsNavTrackers();
return;
}
uint16_t priCol = getThemePrimaryColor();
uint16_t accCol = getThemeAccentColor();
uint16_t c0Col = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t c1Col = isRainbowTheme() ? (uint16_t)0x07E0 : accCol;
uint16_t c2Col = isRainbowTheme() ? (uint16_t)0xFFE0 : priCol;
uint16_t c3Col = isRainbowTheme() ? (uint16_t)0xFD20 : accCol;
uint16_t cardBg = 0x10A2;
bool synced = isGpsSynced();
// 1. Altitude changed: update Card 0 digits only
bool altValid = isGpsAltitudeValid();
int curAltRound = altValid ? (int)round(gpsAltitudeM) : -999;
if (forceRedraw || curAltRound != lastNavAltRound || altValid != lastNavAltValid) {
lastNavAltRound = curAltRound;
lastNavAltValid = altValid;
oldNavAlt = gpsAltitudeM;
int c0X = 4;
int c0Y = 19;
String altStr = altValid ? String(curAltRound) : "---";
drawDseg7RightString34(tft, altStr.c_str(), c0X + 140, c0Y + 28, c0Col, cardBg, 126, 240, isRainbowTheme());
}
// 2. Course / Heading changed: update Card 1
bool hasCourse = synced && isGpsCourseValid();
int curCrsRound = hasCourse ? (int)round(gpsCourseDeg) : -999;
int c1X = 164;
int c1Y = 19;
if (forceRedraw || curCrsRound != lastNavCrsRound || hasCourse != lastNavHasCourse) {
lastNavCrsRound = curCrsRound;
oldNavCourse = gpsCourseDeg;
lastNavHasCourse = hasCourse;
String crsStr = hasCourse ? String(curCrsRound) : "---";
drawDseg7RightString34(tft, crsStr.c_str(), c1X + 130, c1Y + 28, c1Col, cardBg, 85, 240, isRainbowTheme());
if (hasCourse) {
tft.drawCircle(c1X + 137, c1Y + 33, 3, c1Col);
tft.drawCircle(c1X + 137, c1Y + 33, 2, c1Col);
} else {
tft.fillCircle(c1X + 137, c1Y + 33, 4, cardBg);
}
}
// Direction name changed (e.g. NORTE, SUR)
const char* cardDir = hasCourse ? getCompassDirectionFull(gpsCourseDeg) : "---";
if (forceRedraw || strcmp(cardDir, lastNavCardDir) != 0) {
lastNavCardDir = cardDir;
tft.setTextPadding(128);
tft.setTextColor(TFT_WHITE, cardBg);
tft.drawString(cardDir, c1X + 16, c1Y + 74, 2);
tft.setTextPadding(0);
}
// 3. Card 2: Satellites, HDOP and Diagnostic status
int c2X = 4;
int c2Y = 125;
// 3a. Satellites Count (DSEG7 34pt)
bool uartLocked = isGpsUartLocked();
if (forceRedraw || gpsSats != lastNavCardSats || synced != lastNavSatsSync || uartLocked != lastNavUartLocked) {
lastNavCardSats = gpsSats;
lastNavSatsSync = synced;
lastNavUartLocked = uartLocked;
oldNavSats = gpsSats;
String satStr = uartLocked ? String(gpsSats) : "---";
drawDseg7RightString34(tft, satStr.c_str(), c2X + 140, c2Y + 26, c2Col, cardBg, 85, 240, isRainbowTheme());
}
// 3b. HDOP precision
if (forceRedraw || abs(gpsHdop - lastNavCardHdop) >= 0.2f) {
lastNavCardHdop = gpsHdop;
oldNavHdop = gpsHdop;
String hdopStr = "HDOP: " + (synced ? String(gpsHdop, 1) : "---");
tft.setTextPadding(130);
tft.setTextColor(TFT_WHITE, cardBg);
tft.drawString(hdopStr.c_str(), c2X + 16, c2Y + 68, 2);
tft.setTextPadding(0);
}
// 3c. Diagnostic status message ("PRECISION OPTIMA", "SIN SEÑAL GPS", etc.)
// Decoupled: only redrawn when message text actually changes, with setTextPadding (zero flicker!)
const char* curDiag = getGpsDiagMessage();
if (forceRedraw || strcmp(curDiag, lastNavCardDiag) != 0 || synced != lastNavDiagSynced) {
strncpy(lastNavCardDiag, curDiag, sizeof(lastNavCardDiag) - 1);
lastNavCardDiag[sizeof(lastNavCardDiag) - 1] = '\0';
lastNavDiagSynced = synced;
tft.setTextPadding(130);
tft.setTextColor(synced ? 0x9CD3 : c2Col, cardBg);
tft.drawString(curDiag, c2X + 16, c2Y + 90, 1);
tft.setTextPadding(0);
}
// 3d. Large Satellite Icon: only redrawn if waveState or sync changes
uint8_t curWaveState = getGpsWaveAnimState();
if (forceRedraw || curWaveState != oldNavWaveState || synced != oldNavSynced) {
oldNavWaveState = curWaveState;
oldNavSynced = synced;
drawLargeSatelliteIcon(c2X + 16, c2Y + 32, c2Col, curWaveState, cardBg);
}
// 4. Coordinates changed: update Card 3 (setTextPadding avoids fillRect flicker)
if (forceRedraw || abs(gpsLatitude - oldNavLat) >= 0.00005 || abs(gpsLongitude - oldNavLon) >= 0.00005) {
oldNavLat = gpsLatitude;
oldNavLon = gpsLongitude;
int c3X = 164;
int c3Y = 125;
String latStr = gpsLocationValid ? (String(gpsLatitude, 5) + "°") : "--.-----°";
tft.setTextPadding(130);
tft.setTextColor(c3Col, cardBg);
tft.drawString(latStr.c_str(), c3X + 16, c3Y + 38, 2);
String lonStr = gpsLocationValid ? (String(gpsLongitude, 5) + "°") : "--.-----°";
tft.drawString(lonStr.c_str(), c3X + 16, c3Y + 76, 2);
tft.setTextPadding(0);
}
}
// Render Page 3 into double-buffered sprite slice for slide / fade transitions
inline void renderPage3ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) {
uint16_t priCol = getThemePrimaryColor();
uint16_t accCol = getThemeAccentColor();
uint16_t c0Col = isRainbowTheme() ? (uint16_t)0x07FF : priCol;
uint16_t c1Col = isRainbowTheme() ? (uint16_t)0x07E0 : accCol;
uint16_t c2Col = isRainbowTheme() ? (uint16_t)0xFFE0 : priCol;
uint16_t c3Col = isRainbowTheme() ? (uint16_t)0xFD20 : accCol;
uint16_t cardBg = 0x10A2;
uint16_t cardBorder = 0x2945;
bool synced = isGpsSynced();
// 2. CARD 0 (Top Left): ALTITUD (y = 19 to 121)
int c0X = 4 + x_offset;
int c0Y = 19 - y_slice;
int cW = 152;
int cH1 = 102;
if (c0X + cW > 0 && c0X < 320 && c0Y + cH1 > 0 && c0Y < slice_h) {
s.fillRoundRect(c0X, c0Y, cW, cH1, 6, cardBg);
s.drawRoundRect(c0X, c0Y, cW, cH1, 6, cardBorder);
s.fillRoundRect(c0X + 6, c0Y + 8, 4, cH1 - 16, 2, c0Col);
if (c0Y + 24 > 0 && c0Y + 8 < slice_h) {
s.setTextColor(0x8410, cardBg);
s.drawString("ALTITUD", c0X + 16, c0Y + 8, 2);
s.setTextColor(0x9CD3, cardBg);
s.drawString("MSNM", c0X + 96, c0Y + 8, 2);
}
bool altValid = isGpsAltitudeValid();
String altStr = altValid ? String((int)round(gpsAltitudeM)) : "---";
int valY = c0Y + 28;
if (valY < slice_h && valY + 38 > 0) {
drawDseg7RightString34(s, altStr.c_str(), c0X + 140, valY, c0Col, cardBg, 0, slice_h, isRainbowTheme());
}
}
// 3. CARD 1 (Top Right): RUMBO (y = 19 to 121)
int c1X = 164 + x_offset;
int c1Y = 19 - y_slice;
if (c1X + cW > 0 && c1X < 320 && c1Y + cH1 > 0 && c1Y < slice_h) {
s.fillRoundRect(c1X, c1Y, cW, cH1, 6, cardBg);
s.drawRoundRect(c1X, c1Y, cW, cH1, 6, cardBorder);
s.fillRoundRect(c1X + 6, c1Y + 8, 4, cH1 - 16, 2, c1Col);
if (c1Y + 24 > 0 && c1Y + 8 < slice_h) {
s.setTextColor(0x8410, cardBg);
s.drawString("RUMBO / BRUJULA", c1X + 16, c1Y + 8, 2);
}
bool hasCourse = synced && isGpsCourseValid();
String crsStr = hasCourse ? String((int)round(gpsCourseDeg)) : "---";
int valY = c1Y + 28;
if (valY < slice_h && valY + 38 > 0) {
drawDseg7RightString34(s, crsStr.c_str(), c1X + 130, valY, c1Col, cardBg, 0, slice_h, isRainbowTheme());
if (hasCourse) {
int circY = c1Y + 33;
if (circY + 4 > 0 && circY - 4 < slice_h) {
s.drawCircle(c1X + 137, circY, 3, c1Col);
s.drawCircle(c1X + 137, circY, 2, c1Col);
}
}
}
int dirY = c1Y + 74;
if (dirY + 16 > 0 && dirY < slice_h) {
const char* cardDir = hasCourse ? getCompassDirectionFull(gpsCourseDeg) : "---";
s.setTextColor(TFT_WHITE, cardBg);
s.drawString(cardDir, c1X + 16, dirY, 2);
}
}
// 4. CARD 2 (Bottom Left): SATÉLITES GPS (y = 125 to 235)
int c2X = 4 + x_offset;
int c2Y = 125 - y_slice;
int cH2 = 110;
if (c2X + cW > 0 && c2X < 320 && c2Y + cH2 > 0 && c2Y < slice_h) {
s.fillRoundRect(c2X, c2Y, cW, cH2, 6, cardBg);
s.drawRoundRect(c2X, c2Y, cW, cH2, 6, cardBorder);
s.fillRoundRect(c2X + 6, c2Y + 8, 4, cH2 - 16, 2, c2Col);
if (c2Y + 24 > 0 && c2Y + 8 < slice_h) {
s.setTextColor(0x8410, cardBg);
s.drawString("SATELITES GPS", c2X + 16, c2Y + 8, 2);
}
uint8_t waveState = getGpsWaveAnimState();
int icnY = c2Y + 32;
if (icnY + 28 > 0 && icnY < slice_h) {
drawLargeSatelliteIconSprite(s, c2X + 16, icnY, c2Col, waveState, cardBg);
}
int satY = c2Y + 26;
if (satY < slice_h && satY + 38 > 0) {
String satStr = isGpsUartLocked() ? String(gpsSats) : "---";
drawDseg7RightString34(s, satStr.c_str(), c2X + 140, satY, c2Col, cardBg, 0, slice_h, isRainbowTheme());
}
int hdopY = c2Y + 68;
if (hdopY < slice_h && hdopY + 16 > 0) {
String hdopStr = "HDOP: " + (synced ? String(gpsHdop, 1) : "---");
s.setTextColor(TFT_WHITE, cardBg);
s.drawString(hdopStr.c_str(), c2X + 16, hdopY, 2);
}
int qualY = c2Y + 90;
if (qualY + 10 > 0 && qualY < slice_h) {
s.setTextColor(synced ? 0x9CD3 : c2Col, cardBg);
s.drawString(getGpsDiagMessage(), c2X + 16, qualY, 1);
}
}
// 5. CARD 3 (Bottom Right): COORDENADAS (y = 125 to 235)
int c3X = 164 + x_offset;
int c3Y = 125 - y_slice;
if (c3X + cW > 0 && c3X < 320 && c3Y + cH2 > 0 && c3Y < slice_h) {
s.fillRoundRect(c3X, c3Y, cW, cH2, 6, cardBg);
s.drawRoundRect(c3X, c3Y, cW, cH2, 6, cardBorder);
s.fillRoundRect(c3X + 6, c3Y + 8, 4, cH2 - 16, 2, c3Col);
if (c3Y + 24 > 0 && c3Y + 8 < slice_h) {
s.setTextColor(0x8410, cardBg);
s.drawString("COORDENADAS", c3X + 16, c3Y + 8, 2);
}
int latHdrY = c3Y + 26;
if (latHdrY < slice_h && latHdrY + 10 > 0) {
s.setTextColor(0x8410, cardBg);
s.drawString("LAT", c3X + 16, latHdrY, 1);
}
int latValY = c3Y + 38;
if (latValY < slice_h && latValY + 18 > 0) {
String latStr = gpsLocationValid ? (String(gpsLatitude, 5) + "°") : "--.-----°";
s.setTextColor(c3Col, cardBg);
s.drawString(latStr.c_str(), c3X + 16, latValY, 2);
}
int lonHdrY = c3Y + 64;
if (lonHdrY < slice_h && lonHdrY + 10 > 0) {
s.setTextColor(0x8410, cardBg);
s.drawString("LON", c3X + 16, lonHdrY, 1);
}
int lonValY = c3Y + 76;
if (lonValY < slice_h && lonValY + 18 > 0) {
String lonStr = gpsLocationValid ? (String(gpsLongitude, 5) + "°") : "--.-----°";
s.setTextColor(c3Col, cardBg);
s.drawString(lonStr.c_str(), c3X + 16, lonValY, 2);
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -18,18 +18,14 @@ extern XPT2046_TouchscreenSOFTSPI ts;
// Get mapped touchscreen coordinates in landscape (320x240)
bool getTouch(uint16_t *x, uint16_t *y) {
if (!tsSoftSpi)
return false;
if (!tsSoftSpi) return false;
if (ts.touched(tsSoftSpi)) {
TS_Point p = ts.getPoint(tsSoftSpi);
// Filter noise: require valid touch pressure and coordinates within active
// screen
if (p.z < 160)
return false;
if (p.x < 150 || p.x > 3850 || p.y < 150 || p.y > 3850)
return false;
// Filter noise: require valid touch pressure and coordinates within active screen
if (p.z < 160) return false;
if (p.x < 150 || p.x > 3850 || p.y < 150 || p.y > 3850) return false;
int16_t tx = map(p.x, 200, 3700, 0, 320);
int16_t ty = map(p.y, 240, 3800, 0, 240);
@ -43,19 +39,16 @@ bool getTouch(uint16_t *x, uint16_t *y) {
// Direct touch check using pressure Z
inline bool isScreenPressed() {
if (!tsSoftSpi)
return false;
if (!tsSoftSpi) return false;
return ts.touched(tsSoftSpi);
}
// Wait until both touchscreen and selector button are completely released with
// 30ms continuous debounce and 250ms maximum hardware safety timeout
// Wait until both touchscreen and selector button are completely released with 50ms continuous debounce
inline void waitForTouchRelease() {
uint16_t dummyX, dummyY;
uint8_t releaseCount = 0;
uint32_t startWait = millis();
while (releaseCount < 3 && (millis() - startWait < 250)) {
if (getTouch(&dummyX, &dummyY) || (digitalRead(SELECTOR_PIN) == LOW)) {
while (releaseCount < 5) {
if ((tsSoftSpi && ts.touched(tsSoftSpi)) || getTouch(&dummyX, &dummyY) || (digitalRead(SELECTOR_PIN) == LOW)) {
releaseCount = 0;
} else {
releaseCount++;

View file

@ -5,19 +5,14 @@
#include <DallasTemperature.h>
#include <TFT_eSPI.h>
#include "DSEG7_Modern_BoldItalic_34.h"
#include "Latin_Sans_Bold_26.h"
#include "rainbow_text.h"
// Hardware 1-Wire Pin: Conector CN1 Pin 3 (GPIO 22)
#define ONE_WIRE_BUS_PIN 22
extern TFT_eSPI tft;
extern uint16_t getThemePrimaryColor();
extern uint16_t getThemeAccentColor();
extern uint16_t getThemeBgColor();
extern bool isRainbowTheme();
extern void touchBeep();
extern String warningValue[5];
// Global states for multi-page system
extern int8_t fullScreenCell;
@ -38,44 +33,12 @@ static DallasTemperature dsSensors(&oneWireObj);
static float oldTriCoolant = -999.0f;
static float oldTriInterior = -999.0f;
static float oldTriExterior = -999.0f;
static bool oldTriIgnition = true;
static uint8_t oldTriDsSensorCount = 255;
// Pre-rendered smooth title sprites for Page 1 (AGUA, INTERIOR, EXTERIOR)
// 100% anti-aliased Latin_Sans_Bold_26: 0 mallocs during transitions and zero flicker
static TFT_eSprite sprTriTitle[3] = { TFT_eSprite(&tft), TFT_eSprite(&tft), TFT_eSprite(&tft) };
static bool sprTriTitleReady = false;
inline void initTriTempTitles() {
if (sprTriTitleReady) return;
uint16_t cardBg = 0x10A2;
const char* titles[3] = { "AGUA", "INTERIOR", "EXTERIOR" };
const int widths[3] = { 80, 136, 136 };
for (int i = 0; i < 3; i++) {
if (sprTriTitle[i].createSprite(widths[i], 26) != nullptr) {
sprTriTitle[i].loadFont(Latin_Sans_Bold_26);
sprTriTitle[i].setTextPadding(0);
sprTriTitle[i].fillSprite(cardBg);
sprTriTitle[i].setTextColor(TFT_WHITE, cardBg);
sprTriTitle[i].drawString(titles[i], 0, 0, 1);
sprTriTitle[i].unloadFont();
if (isRainbowTheme()) {
applyRainbowToSprite(sprTriTitle[i], 4 + 16, 19 + i * 74 + 5, cardBg);
}
}
}
sprTriTitleReady = true;
}
// Initialize 1-Wire sensors
inline void initTriTemp() {
initTriTempTitles();
dsSensors.begin();
dsSensors.setWaitForConversion(false); // Non-blocking conversion (0ms wait)
dsSensorCount = dsSensors.getDeviceCount();
oldTriDsSensorCount = dsSensorCount;
oldTriIgnition = ignitionOn;
if (dsSensorCount > 0) {
dsSensors.requestTemperatures();
}
@ -84,120 +47,10 @@ inline void initTriTemp() {
#endif
}
// 8-bit anti-aliased alpha mask for digital 7-segment italic "°C" (30x26)
const uint8_t s_dsegUnitAlpha[30 * 26] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x10, 0x11, 0x11, 0x12, 0x0E, 0x00, 0x01, 0x00,
0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x7A, 0xED, 0xEC, 0xED, 0xED, 0xE4, 0x2A, 0x00, 0x02,
0x02, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x10, 0x11, 0x03, 0x00, 0x01,
0x00, 0x00, 0x01, 0x02, 0x0C, 0xA2, 0xFF, 0xF2, 0xF3, 0xF4, 0xF1, 0xFF, 0xD2, 0x12, 0x04,
0x00, 0x01, 0x97, 0xEE, 0xEC, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEC, 0xEE, 0xC1, 0x13, 0x01,
0x00, 0x00, 0x04, 0x00, 0x5C, 0xFF, 0x9F, 0x10, 0x14, 0x10, 0x13, 0xD9, 0xFF, 0x2B, 0x00,
0x12, 0xB6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x77, 0x00,
0x00, 0x00, 0x04, 0x00, 0x92, 0xFC, 0x63, 0x00, 0x01, 0x00, 0x19, 0xFC, 0xE1, 0x05, 0x31,
0xD8, 0xFF, 0xFC, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFB, 0xF9, 0x89, 0x0B, 0x02,
0x00, 0x00, 0x01, 0x00, 0xC8, 0xFF, 0x38, 0x05, 0x0B, 0x05, 0x4E, 0xFF, 0xAD, 0x00, 0xD4,
0xFF, 0xFC, 0x66, 0x27, 0x2A, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x23, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x15, 0xF3, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x79, 0xFF, 0x73, 0x09, 0xF8,
0xFD, 0xF6, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00,
0x00, 0x02, 0x00, 0x2A, 0xFF, 0xDF, 0x7A, 0x7C, 0x7D, 0x7A, 0xDB, 0xFC, 0x28, 0x3B, 0xFF,
0xFF, 0xD5, 0x02, 0x02, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0x00, 0x84, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0x46, 0x00, 0x7E, 0xFF,
0xFF, 0xA3, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x70, 0x7B, 0x7A, 0x7A, 0x79, 0x22, 0x00, 0x00, 0xAF, 0xFF,
0xFF, 0x6E, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0xDF, 0xFF,
0xFF, 0x3A, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x27, 0xFB, 0xFF,
0xED, 0x11, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x57, 0xFF, 0xFF,
0xC5, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x8A, 0xFF, 0xFF,
0x91, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xC0, 0xFF, 0xFF,
0x5D, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0C, 0xE9, 0xFF, 0xFC,
0x2D, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x34, 0xFF, 0xFF, 0xE4,
0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x67, 0xFF, 0xFF, 0xB6,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x9C, 0xFF, 0xFF, 0x82,
0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0xCF, 0xFF, 0xFF, 0x4D,
0x01, 0x04, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x16, 0xF2, 0xFF, 0xF7, 0x1A,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x4B, 0xFF, 0xFE, 0xD8, 0x12,
0x12, 0x12, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x20, 0xE4, 0xFF, 0xF7, 0xED,
0xEE, 0xED, 0xED, 0xED, 0xED, 0xED, 0xEB, 0xED, 0x93, 0x05, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0xFA, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3D, 0x00, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x67, 0xF9, 0xF6,
0xF9, 0xF9, 0xF9, 0xF9, 0xF9, 0xF8, 0xF7, 0xE9, 0x5B, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x28, 0x2A,
0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x29, 0x2A, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// Draw 100% anti-aliased digital 7-segment italic unit "°C" matching DSEG7 typography and color
inline void draw7SegDegreeC(TFT_eSPI &dst, int bx, int by, uint16_t color = 0xC618, uint16_t bgCol = 0x10A2) {
for (int j = 0; j < 26; j++) {
int py = by + j;
if (py < 0 || py >= dst.height()) continue;
int rowOffset = j * 30;
for (int i = 0; i < 30; i++) {
int px = bx + i;
if (px < 0 || px >= dst.width()) continue;
uint8_t a = pgm_read_byte(&s_dsegUnitAlpha[rowOffset + i]);
if (a > 8) {
uint16_t pixCol = (a >= 250) ? color : fastBlend(a, color, bgCol);
dst.drawPixel(px, py, pixCol);
}
}
}
}
// Double-buffered 120x8 progress bar sprite (zero jitter)
static TFT_eSprite sprBar(&tft);
static bool sprBarCreated = false;
static int oldTriFillW[3] = { -1, -1, -1 };
static uint16_t oldTriAccentCol[3] = { 0, 0, 0 };
inline void drawTriTempBar(int barX, int barY, int fillW, uint16_t accentCol) {
if (!sprBarCreated) {
sprBarCreated = (sprBar.createSprite(120, 8) != nullptr);
}
if (sprBarCreated) {
sprBar.fillRoundRect(0, 0, 120, 8, 3, 0x18C3);
if (fillW > 0) {
if (isRainbowTheme()) {
for (int bx = 0; bx < fillW; bx++) {
sprBar.drawFastVLine(bx, 0, 8, getRainbowScreenColor(barX + bx, barY));
}
} else {
sprBar.fillRoundRect(0, 0, fillW, 8, 3, accentCol);
}
}
sprBar.pushSprite(barX, barY);
} else {
tft.fillRoundRect(barX, barY, 120, 8, 3, 0x18C3);
if (fillW > 0) {
if (isRainbowTheme()) {
for (int bx = 0; bx < fillW; bx++) {
tft.drawFastVLine(barX + bx, barY, 8, getRainbowScreenColor(barX + bx, barY));
}
} else {
tft.fillRoundRect(barX, barY, fillW, 8, 3, accentCol);
}
}
}
}
// Draw a single temperature card with card index (0: Motor, 1: Interior, 2: Exterior)
inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) {
int cardY = 19 + cardIdx * 74; // y = 19, 93, 167 (bounds 19 to 235 matching clock screen)
int cardH = 68;
int cardY = 6 + cardIdx * 77; // y = 6, 83, 160
int cardH = 73;
int cardX = 4 + x_offset;
int cardW = 312;
@ -218,67 +71,51 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) {
bool isInt = false;
if (cardIdx == 0) {
titleStr = "AGUA";
titleStr = "MOTOR / REFRIGERANTE";
subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)";
minRange = 60.0f;
maxRange = 120.0f;
isInt = true;
float warnT = warningValue[0].toFloat();
if (warnT <= 0) warnT = 110.0f;
if (val < (warnT - 10.0f)) accentCol = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor();
else if (val < warnT) accentCol = TFT_YELLOW;
if (val < 100.0f) accentCol = getThemePrimaryColor();
else if (val < 110.0f) accentCol = TFT_YELLOW;
else accentCol = TFT_RED;
} else if (cardIdx == 1) {
titleStr = "INTERIOR";
subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" : "DS18B20 SENSOR 1";
titleStr = "HABITACULO / INTERIOR";
subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" : "DS18B20 SENSOR 1 (DEMO)";
minRange = 10.0f;
maxRange = 40.0f;
accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor(); // Green in rainbow
accentCol = 0x07F3; // Mint Green
} else {
titleStr = "EXTERIOR";
subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2 (GPIO 22)" : (dsSensorCount == 1 ? "ESTIMADA (-4.5`C)" : "DS18B20 SENSOR 2");
titleStr = "AMBIENTE / EXTERIOR";
subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2 (GPIO 22)" : (dsSensorCount == 1 ? "ESTIMADA (-4.5`C)" : "DS18B20 SENSOR 2 (DEMO)");
minRange = -10.0f;
maxRange = 50.0f;
accentCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor(); // Orange in rainbow
accentCol = 0x3D7F; // Sky Blue
}
// Left vertical accent bar
tft.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol);
tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol);
// Titles (Smooth anti-aliased font)
if (!sprTriTitleReady) initTriTempTitles();
if (sprTriTitleReady && sprTriTitle[cardIdx].created()) {
sprTriTitle[cardIdx].pushSprite(cardX + 16, cardY + 5);
} else {
tft.loadFont(Latin_Sans_Bold_26);
tft.setTextPadding(0);
// Titles
tft.setTextColor(TFT_WHITE, cardBg);
tft.drawString(titleStr, cardX + 16, cardY + 5, 1);
tft.unloadFont();
}
tft.drawString(titleStr, cardX + 16, cardY + 8, 2);
tft.setTextColor(0x8410, cardBg); // Soft gray
tft.drawString(subStr, cardX + 16, cardY + 32, 1);
tft.drawString(subStr, cardX + 16, cardY + 28, 1);
// 3. Horizontal Progress Gauge Bar
int barX = cardX + 16;
int barY = cardY + 48;
int barY = cardY + 50;
int barW = 120;
int barH = 8;
tft.fillRoundRect(barX, barY, barW, barH, 3, 0x18C3); // Trough background
float ratio = (val - minRange) / (maxRange - minRange);
if (ratio < 0.0f) ratio = 0.0f;
if (ratio > 1.0f) ratio = 1.0f;
int fillW = (int)(barW * ratio);
if (fillW < 4) fillW = 4;
if (x_offset == 0) {
drawTriTempBar(barX, barY, fillW, accentCol);
oldTriFillW[cardIdx] = fillW;
oldTriAccentCol[cardIdx] = accentCol;
} else {
tft.fillRoundRect(barX, barY, barW, 8, 3, 0x18C3);
tft.fillRoundRect(barX, barY, fillW, 8, 3, accentCol);
}
tft.fillRoundRect(barX, barY, fillW, barH, 3, accentCol);
// 4. Digital Number using DSEG7 Modern Bold Italic 34pt
String numStr;
@ -290,130 +127,16 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) {
numStr = String(val, 1);
}
bool rainbowTxt = isRainbowTheme() && (accentCol != TFT_RED);
drawDseg7RightString34(tft, numStr.c_str(), cardX + 262, cardY + 20, accentCol, cardBg, 100, 240, rainbowTxt);
tft.loadFont(DSEG7_Modern_BoldItalic_34);
tft.setTextColor(accentCol, cardBg);
tft.setTextPadding(100);
tft.drawRightString(numStr.c_str(), cardX + 262, cardY + 22, 1);
tft.setTextPadding(0);
tft.unloadFont();
// Unit "°C" in authentic digital 7-segment italic typography matching numbers
draw7SegDegreeC(tft, cardX + 268, cardY + 22, isRainbowTheme() ? getRainbowScreenColor(cardX + 268, cardY + 22) : 0xC618, cardBg);
}
// Flicker-free dynamic value update for a single temperature card
inline void updateTriTempCardValue(uint8_t cardIdx, float val, float oldVal) {
if (currentScreenPage != 1 || fullScreenCell != -1) return;
int cardY = 19 + cardIdx * 74; // y = 19, 93, 167
int cardH = 68;
int cardX = 4;
uint16_t cardBg = 0x10A2; // Dark graphite navy
float minRange = 0.0f, maxRange = 100.0f;
bool isInt = false;
uint16_t accentCol = getThemePrimaryColor();
uint16_t oldAccentCol = getThemePrimaryColor();
if (cardIdx == 0) {
minRange = 60.0f;
maxRange = 120.0f;
isInt = true;
float warnT = warningValue[0].toFloat();
if (warnT <= 0) warnT = 110.0f;
if (val < (warnT - 10.0f)) accentCol = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor();
else if (val < warnT) accentCol = TFT_YELLOW;
else accentCol = TFT_RED;
if (oldVal < (warnT - 10.0f)) oldAccentCol = isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor();
else if (oldVal < warnT) oldAccentCol = TFT_YELLOW;
else oldAccentCol = TFT_RED;
// Subtitle update if ignition state changed
if (ignitionOn != oldTriIgnition) {
oldTriIgnition = ignitionOn;
const char* subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)";
tft.fillRect(cardX + 16, cardY + 32, 130, 10, cardBg);
tft.setTextColor(0x8410, cardBg);
tft.drawString(subStr, cardX + 16, cardY + 32, 1);
}
} else if (cardIdx == 1) {
minRange = 10.0f;
maxRange = 40.0f;
accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor();
oldAccentCol = accentCol;
} else {
minRange = -10.0f;
maxRange = 50.0f;
accentCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor();
oldAccentCol = accentCol;
}
// 1. Accent color changed: update vertical accent bar
if (accentCol != oldAccentCol) {
tft.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol);
}
// 2. Horizontal Progress Gauge Bar update (only redraw when pixel fill changes or accent changes)
int barX = cardX + 16;
int barY = cardY + 48;
int barW = 120;
float ratio = (val - minRange) / (maxRange - minRange);
if (ratio < 0.0f) ratio = 0.0f;
if (ratio > 1.0f) ratio = 1.0f;
int fillW = (int)(barW * ratio);
if (fillW < 4) fillW = 4;
if (fillW != oldTriFillW[cardIdx] || accentCol != oldTriAccentCol[cardIdx]) {
drawTriTempBar(barX, barY, fillW, accentCol);
oldTriFillW[cardIdx] = fillW;
oldTriAccentCol[cardIdx] = accentCol;
}
// 3. Digital Number update using DSEG7 Modern Bold Italic 34pt (setTextPadding prevents flicker)
String numStr;
if (val <= -900.0f) {
numStr = "---";
} else if (isInt) {
numStr = String((int)val);
} else {
numStr = String(val, 1);
}
bool rainbowTxt = isRainbowTheme() && (accentCol != TFT_RED);
drawDseg7RightString34(tft, numStr.c_str(), cardX + 262, cardY + 20, accentCol, cardBg, 100, 240, rainbowTxt);
}
// Synchronize tracking variables without touching TFT display (zero flicker)
inline void syncTriTempTrackers() {
oldTriCoolant = tempEngineCoolant;
oldTriInterior = tempInterior;
oldTriExterior = tempExterior;
oldTriIgnition = ignitionOn;
oldTriDsSensorCount = dsSensorCount;
float vals[3] = { tempEngineCoolant, tempInterior, tempExterior };
float minR[3] = { 60.0f, 10.0f, -10.0f };
float maxR[3] = { 120.0f, 40.0f, 50.0f };
uint16_t defAcc[3] = {
isRainbowTheme() ? (uint16_t)0x07FF : getThemePrimaryColor(),
isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor(),
isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor()
};
for (int i = 0; i < 3; i++) {
float ratio = (vals[i] - minR[i]) / (maxR[i] - minR[i]);
if (ratio < 0.0f) ratio = 0.0f;
if (ratio > 1.0f) ratio = 1.0f;
int fillW = (int)(120 * ratio);
if (fillW < 4) fillW = 4;
oldTriFillW[i] = fillW;
uint16_t acc = defAcc[i];
if (i == 0) {
float warnT = warningValue[0].toFloat();
if (warnT <= 0) warnT = 110.0f;
if (vals[i] >= warnT) acc = TFT_RED;
else if (vals[i] >= (warnT - 10.0f)) acc = TFT_YELLOW;
}
oldTriAccentCol[i] = acc;
}
// Unit "°C"
tft.setTextColor(0xC618, cardBg);
tft.drawString("`C", cardX + 268, cardY + 32, 2);
}
// Draw full Tri-Temp screen
@ -428,21 +151,21 @@ inline void drawTriTempScreen(int x_offset = 0) {
drawTriTempCard(1, tempInterior, x_offset);
drawTriTempCard(2, tempExterior, x_offset);
syncTriTempTrackers();
oldTriCoolant = tempEngineCoolant;
oldTriInterior = tempInterior;
oldTriExterior = tempExterior;
// Draw Page Indicator: [○ ● ○ ○] (Page 2 of 4 active)
tft.fillCircle(139 + x_offset, 2, 2, 0x5AEB); // Page 1 inactive
tft.fillCircle(153 + x_offset, 2, 2, getThemePrimaryColor()); // Page 2 active
tft.fillCircle(167 + x_offset, 2, 2, 0x5AEB); // Page 3 inactive
tft.fillCircle(181 + x_offset, 2, 2, 0x5AEB); // Page 4 inactive
// Draw Page Indicator: [○ ●] (Page 2 active)
tft.fillCircle(153, 2, 2, 0x5AEB); // Page 1 inactive
tft.fillCircle(167, 2, 2, getThemePrimaryColor()); // Page 2 active
}
// Update coolant reading dynamically without redrawing entire screen
inline void updateTriTempCoolant(float val) {
tempEngineCoolant = val;
if (currentScreenPage != 1 || fullScreenCell != -1) return;
if (val != oldTriCoolant || ignitionOn != oldTriIgnition) {
updateTriTempCardValue(0, val, oldTriCoolant);
if (val != oldTriCoolant) {
drawTriTempCard(0, val, 0);
oldTriCoolant = val;
}
}
@ -486,24 +209,15 @@ inline void updateTriTempSensors(bool forceRedraw = false) {
}
if (currentScreenPage == 1 && fullScreenCell == -1) {
// If sensor count changed (e.g. plug / unplug), redraw full cards so subtitles update
if (dsSensorCount != oldTriDsSensorCount) {
oldTriDsSensorCount = dsSensorCount;
drawTriTempCard(1, tempInterior, 0);
drawTriTempCard(2, tempExterior, 0);
oldTriInterior = tempInterior;
oldTriExterior = tempExterior;
} else {
if (forceRedraw || abs(tempInterior - oldTriInterior) >= 0.1f) {
updateTriTempCardValue(1, tempInterior, oldTriInterior);
drawTriTempCard(1, tempInterior, 0);
oldTriInterior = tempInterior;
}
if (forceRedraw || abs(tempExterior - oldTriExterior) >= 0.1f) {
updateTriTempCardValue(2, tempExterior, oldTriExterior);
drawTriTempCard(2, tempExterior, 0);
oldTriExterior = tempExterior;
}
}
}
}
// Draw fast slide frame of Page 1 (3 temperature cards) shifted by x_offset