Compare commits
4 commits
100d19762e
...
4c5967d3c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c5967d3c5 | |||
| cb77100a49 | |||
| 4937cf3b42 | |||
| d124d013ab |
44
README.md
|
|
@ -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 (PID 010D):** Rango 0 a 160 km/h.
|
||||
* **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).
|
||||
* **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,7 +27,32 @@ 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. Audio y Melodías de Alarma
|
||||
### 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
|
||||
* 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).
|
||||
|
|
@ -35,7 +60,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.
|
||||
|
||||
### 5. Menú de Configuración Táctil
|
||||
### 8. 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%).
|
||||
|
|
@ -50,11 +75,18 @@ 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. **Escáner OBD-II Bluetooth:**
|
||||
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:**
|
||||
* Adaptador ELM327 v1.5 (muy recomendado con chip PIC18F25K80, evitar clones v2.1 de baja calidad).
|
||||
3. **Buzzer / Parlante:**
|
||||
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:**
|
||||
* Parlantito o buzzer pasivo de 8 ohms conectado al pin de audio del CYD (controlado por canal PWM con modulación de volumen).
|
||||
4. **Alimentación:**
|
||||
6. **Alimentación:**
|
||||
* Módulo convertidor 12V a 5V USB (conectado a positivo bajo llave o directo si se utiliza auto-sleep).
|
||||
|
||||
---
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 98 KiB |
|
|
@ -37,3 +37,29 @@ 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`.*
|
||||
|
||||
|
|
|
|||
149
emulator/gps_emulator.py
Executable file
|
|
@ -0,0 +1,149 @@
|
|||
#!/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)
|
||||
|
|
@ -359,6 +359,79 @@ 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:
|
||||
|
|
@ -370,6 +443,10 @@ 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":
|
||||
|
|
@ -392,6 +469,15 @@ 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()
|
||||
|
|
|
|||
BIN
gps_icon.jpg
Normal file
|
After Width: | Height: | Size: 1,003 KiB |
44
gps_icon.svg
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
gps_pinout.jpg
Normal file
|
After Width: | Height: | Size: 1 MiB |
180
hardware/GPS_N32_INTEGRACION.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# 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
|
||||
|
||||

|
||||
|
||||
* **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)
|
||||
|
||||

|
||||
|
||||
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.
|
||||
131
hardware/create_annotated_photo.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#!/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)
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 98 KiB |
BIN
hardware/diagrama_conexion_hardware.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
305
hardware/generate_diagram.py
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
#!/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!")
|
||||
216
hardware/generate_gps_pinout.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
#!/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)
|
||||
BIN
hardware/gps_pinout.jpg
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
hardware/gps_pinout.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
hardware/pinout_gps.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
hardware/pinout_gps_foto_anotada.png
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
pinout_gps.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
pinout_gps_foto_anotada.png
Normal file
|
After Width: | Height: | Size: 1 MiB |
|
|
@ -1,3 +1,4 @@
|
|||
#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,
|
||||
|
|
|
|||
1293
sketch/obd2_gauge/Latin_Sans_Bold_26.h
Normal file
|
|
@ -2,7 +2,8 @@
|
|||
//----------------------------------
|
||||
extern bool ignitionOn;
|
||||
extern bool wokeFromSleep;
|
||||
void initScreen();
|
||||
extern TaskHandle_t btTaskHandle;
|
||||
void initScreen(bool redrawTFT = true);
|
||||
void plotNoData(uint8_t cell);
|
||||
void touchBeep();
|
||||
|
||||
|
|
@ -26,7 +27,9 @@ 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!
|
||||
|
|
@ -34,7 +37,9 @@ 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++;
|
||||
|
|
@ -95,7 +100,10 @@ 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 {
|
||||
|
|
@ -109,12 +117,16 @@ 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();
|
||||
|
|
@ -126,13 +138,17 @@ 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;
|
||||
|
|
@ -140,46 +156,15 @@ 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() {
|
||||
setRearLedColor(0, 0, 200);//blue led on
|
||||
String txt = "Connecting to " + client_name +" - " + ByteArraytoString(recent_client_addr);
|
||||
Terminal(txt,0,48,320,191);
|
||||
Serial.println(txt);
|
||||
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)) {
|
||||
Serial.print(F("."));
|
||||
blink =! blink;
|
||||
if (blink) setRearLedColor(0, 0, 200);
|
||||
else setRearLedOff();
|
||||
try_count++;
|
||||
}
|
||||
if (try_count == 3) {//cannot connect
|
||||
Terminal("OBDII Adaptor not found!",0,48,320,191);
|
||||
Serial.println(F("OBDII Adaptor not found!"));
|
||||
BTSerial.disconnect();
|
||||
setRearLedOff();
|
||||
foundOBD2 = false;
|
||||
} else {
|
||||
Terminal("Connected Successfully!",0,48,320,191);
|
||||
Serial.println(F("Connected Successfully!"));
|
||||
prompt = true;
|
||||
updateRearLedTheme(); // Set rear RGB LED to active theme color
|
||||
foundOBD2 = true;
|
||||
}
|
||||
|
||||
}//connectLasbtOBDII
|
||||
|
||||
//---------------------------
|
||||
// Resume Bluetooth connection with OBDII scanner upon waking from sleep
|
||||
bool resumeBluetoothConnection() {
|
||||
bool hasRecentAddr = false;
|
||||
for (uint8_t i = 0; i < 6; i++) {
|
||||
if (recent_client_addr[i] != 0) {
|
||||
|
|
@ -188,89 +173,59 @@ bool resumeBluetoothConnection() {
|
|||
}
|
||||
}
|
||||
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 false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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 < 4)) {
|
||||
blink = !blink;
|
||||
while(!BTSerial.connected(1000) && (try_count <3)) {
|
||||
#ifdef SERIAL_DEBUG
|
||||
Serial.print(F("."));
|
||||
#endif
|
||||
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);
|
||||
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;
|
||||
delay(400);
|
||||
initScreen();
|
||||
for (uint8_t c = 0; c < 4; c++) {
|
||||
plotNoData(c);
|
||||
} 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;
|
||||
}
|
||||
lastActivityTime = millis();
|
||||
return false;
|
||||
|
||||
}//connectLasbtOBDII
|
||||
|
||||
//---------------------------
|
||||
//---------------------------
|
||||
// Resume Bluetooth connection with OBDII scanner upon waking from sleep (en segundo plano, 100% no bloqueante)
|
||||
bool resumeBluetoothConnection() {
|
||||
foundOBD2 = false;
|
||||
if (btTaskHandle != NULL) {
|
||||
xTaskNotifyGive(btTaskHandle);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
482
sketch/obd2_gauge/gps_clock.h
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
sketch/obd2_gauge/gps_icon_aa_data.h
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// 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,
|
||||
};
|
||||
626
sketch/obd2_gauge/gps_module.h
Normal file
|
|
@ -0,0 +1,626 @@
|
|||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
475
sketch/obd2_gauge/gps_nav.h
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
sketch/obd2_gauge/rainbow_text.h
Normal file
|
|
@ -18,14 +18,18 @@ 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);
|
||||
|
|
@ -39,16 +43,19 @@ 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 50ms continuous debounce
|
||||
// Wait until both touchscreen and selector button are completely released with
|
||||
// 30ms continuous debounce and 250ms maximum hardware safety timeout
|
||||
inline void waitForTouchRelease() {
|
||||
uint16_t dummyX, dummyY;
|
||||
uint8_t releaseCount = 0;
|
||||
while (releaseCount < 5) {
|
||||
if ((tsSoftSpi && ts.touched(tsSoftSpi)) || getTouch(&dummyX, &dummyY) || (digitalRead(SELECTOR_PIN) == LOW)) {
|
||||
uint32_t startWait = millis();
|
||||
while (releaseCount < 3 && (millis() - startWait < 250)) {
|
||||
if (getTouch(&dummyX, &dummyY) || (digitalRead(SELECTOR_PIN) == LOW)) {
|
||||
releaseCount = 0;
|
||||
} else {
|
||||
releaseCount++;
|
||||
|
|
|
|||
|
|
@ -5,14 +5,19 @@
|
|||
#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;
|
||||
|
|
@ -33,12 +38,44 @@ 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();
|
||||
}
|
||||
|
|
@ -47,10 +84,120 @@ 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 = 6 + cardIdx * 77; // y = 6, 83, 160
|
||||
int cardH = 73;
|
||||
int cardY = 19 + cardIdx * 74; // y = 19, 93, 167 (bounds 19 to 235 matching clock screen)
|
||||
int cardH = 68;
|
||||
int cardX = 4 + x_offset;
|
||||
int cardW = 312;
|
||||
|
||||
|
|
@ -71,51 +218,67 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) {
|
|||
bool isInt = false;
|
||||
|
||||
if (cardIdx == 0) {
|
||||
titleStr = "MOTOR / REFRIGERANTE";
|
||||
titleStr = "AGUA";
|
||||
subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)";
|
||||
minRange = 60.0f;
|
||||
maxRange = 120.0f;
|
||||
isInt = true;
|
||||
if (val < 100.0f) accentCol = getThemePrimaryColor();
|
||||
else if (val < 110.0f) accentCol = TFT_YELLOW;
|
||||
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;
|
||||
} else if (cardIdx == 1) {
|
||||
titleStr = "HABITACULO / INTERIOR";
|
||||
subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" : "DS18B20 SENSOR 1 (DEMO)";
|
||||
titleStr = "INTERIOR";
|
||||
subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" : "DS18B20 SENSOR 1";
|
||||
minRange = 10.0f;
|
||||
maxRange = 40.0f;
|
||||
accentCol = 0x07F3; // Mint Green
|
||||
accentCol = isRainbowTheme() ? (uint16_t)0x07E0 : getThemeAccentColor(); // Green in rainbow
|
||||
} else {
|
||||
titleStr = "AMBIENTE / EXTERIOR";
|
||||
subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2 (GPIO 22)" : (dsSensorCount == 1 ? "ESTIMADA (-4.5`C)" : "DS18B20 SENSOR 2 (DEMO)");
|
||||
titleStr = "EXTERIOR";
|
||||
subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2 (GPIO 22)" : (dsSensorCount == 1 ? "ESTIMADA (-4.5`C)" : "DS18B20 SENSOR 2");
|
||||
minRange = -10.0f;
|
||||
maxRange = 50.0f;
|
||||
accentCol = 0x3D7F; // Sky Blue
|
||||
accentCol = isRainbowTheme() ? (uint16_t)0xFD20 : getThemePrimaryColor(); // Orange in rainbow
|
||||
}
|
||||
|
||||
// Left vertical accent bar
|
||||
tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol);
|
||||
tft.fillRoundRect(cardX + 6, cardY + 6, 4, cardH - 12, 2, accentCol);
|
||||
|
||||
// Titles
|
||||
// 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);
|
||||
tft.setTextColor(TFT_WHITE, cardBg);
|
||||
tft.drawString(titleStr, cardX + 16, cardY + 8, 2);
|
||||
tft.drawString(titleStr, cardX + 16, cardY + 5, 1);
|
||||
tft.unloadFont();
|
||||
}
|
||||
|
||||
tft.setTextColor(0x8410, cardBg); // Soft gray
|
||||
tft.drawString(subStr, cardX + 16, cardY + 28, 1);
|
||||
tft.drawString(subStr, cardX + 16, cardY + 32, 1);
|
||||
|
||||
// 3. Horizontal Progress Gauge Bar
|
||||
int barX = cardX + 16;
|
||||
int barY = cardY + 50;
|
||||
int barY = cardY + 48;
|
||||
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;
|
||||
tft.fillRoundRect(barX, barY, fillW, barH, 3, accentCol);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 4. Digital Number using DSEG7 Modern Bold Italic 34pt
|
||||
String numStr;
|
||||
|
|
@ -127,16 +290,130 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) {
|
|||
numStr = String(val, 1);
|
||||
}
|
||||
|
||||
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();
|
||||
bool rainbowTxt = isRainbowTheme() && (accentCol != TFT_RED);
|
||||
drawDseg7RightString34(tft, numStr.c_str(), cardX + 262, cardY + 20, accentCol, cardBg, 100, 240, rainbowTxt);
|
||||
|
||||
// Unit "°C"
|
||||
tft.setTextColor(0xC618, cardBg);
|
||||
tft.drawString("`C", cardX + 268, cardY + 32, 2);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw full Tri-Temp screen
|
||||
|
|
@ -151,21 +428,21 @@ inline void drawTriTempScreen(int x_offset = 0) {
|
|||
drawTriTempCard(1, tempInterior, x_offset);
|
||||
drawTriTempCard(2, tempExterior, x_offset);
|
||||
|
||||
oldTriCoolant = tempEngineCoolant;
|
||||
oldTriInterior = tempInterior;
|
||||
oldTriExterior = tempExterior;
|
||||
syncTriTempTrackers();
|
||||
|
||||
// Draw Page Indicator: [○ ●] (Page 2 active)
|
||||
tft.fillCircle(153, 2, 2, 0x5AEB); // Page 1 inactive
|
||||
tft.fillCircle(167, 2, 2, getThemePrimaryColor()); // Page 2 active
|
||||
// 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
|
||||
}
|
||||
|
||||
// Update coolant reading dynamically without redrawing entire screen
|
||||
inline void updateTriTempCoolant(float val) {
|
||||
tempEngineCoolant = val;
|
||||
if (currentScreenPage != 1 || fullScreenCell != -1) return;
|
||||
if (val != oldTriCoolant) {
|
||||
drawTriTempCard(0, val, 0);
|
||||
if (val != oldTriCoolant || ignitionOn != oldTriIgnition) {
|
||||
updateTriTempCardValue(0, val, oldTriCoolant);
|
||||
oldTriCoolant = val;
|
||||
}
|
||||
}
|
||||
|
|
@ -209,15 +486,24 @@ inline void updateTriTempSensors(bool forceRedraw = false) {
|
|||
}
|
||||
|
||||
if (currentScreenPage == 1 && fullScreenCell == -1) {
|
||||
if (forceRedraw || abs(tempInterior - oldTriInterior) >= 0.1f) {
|
||||
// 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);
|
||||
oldTriInterior = tempInterior;
|
||||
}
|
||||
if (forceRedraw || abs(tempExterior - oldTriExterior) >= 0.1f) {
|
||||
drawTriTempCard(2, tempExterior, 0);
|
||||
updateTriTempCardValue(2, tempExterior, oldTriExterior);
|
||||
oldTriExterior = tempExterior;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw fast slide frame of Page 1 (3 temperature cards) shifted by x_offset
|
||||
|
|
|
|||