diff --git a/README.md b/README.md index 2892f23..87d9e2d 100644 --- a/README.md +++ b/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). --- diff --git a/diagrama_conexion_ds18b20.png b/diagrama_conexion_ds18b20.png index f1ad8d4..383e374 100644 Binary files a/diagrama_conexion_ds18b20.png and b/diagrama_conexion_ds18b20.png differ diff --git a/emulator/README.md b/emulator/README.md index eb99f27..d99779b 100644 --- a/emulator/README.md +++ b/emulator/README.md @@ -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`.* + diff --git a/emulator/gps_emulator.py b/emulator/gps_emulator.py new file mode 100755 index 0000000..1e96ae1 --- /dev/null +++ b/emulator/gps_emulator.py @@ -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) diff --git a/emulator/obd2_emulator.py b/emulator/obd2_emulator.py index bf93851..12bd5b1 100755 --- a/emulator/obd2_emulator.py +++ b/emulator/obd2_emulator.py @@ -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() diff --git a/gps_icon.jpg b/gps_icon.jpg new file mode 100644 index 0000000..2af4bd3 Binary files /dev/null and b/gps_icon.jpg differ diff --git a/gps_icon.svg b/gps_icon.svg new file mode 100644 index 0000000..4cf5a89 --- /dev/null +++ b/gps_icon.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gps_pinout.jpg b/gps_pinout.jpg new file mode 100644 index 0000000..3fac66c Binary files /dev/null and b/gps_pinout.jpg differ diff --git a/hardware/GPS_N32_INTEGRACION.md b/hardware/GPS_N32_INTEGRACION.md new file mode 100644 index 0000000..df3cad1 --- /dev/null +++ b/hardware/GPS_N32_INTEGRACION.md @@ -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 + +![Pinout del Módulo GPS U-blox N32+](pinout_gps.png) + +* **Chipset**: U-blox 7 / NEO-7M (o compatible). +* **Protocolo de salida**: NMEA 0183 (mensajes $GPRMC, $GPGGA, $GPVTG). +* **Protocolo de configuración**: Binario U-blox (UBX). +* **Velocidad predeterminada (Baudrate)**: 9600 bps (8N1). +* **Voltaje de alimentación**: 5V (conectado a `Vin` del conector P1). +* **Nivel lógico UART**: 3.3V (compatible directo con ESP32 sin conversores). +* **Consumo activo**: ~45 a 60 mA. +* **Consumo en modo reposo (Software Standby / Backup)**: ~0.3 a 0.5 mA. + +--- + +## 2. Esquema de Conexión Física (Bidireccional) + +![Diagrama de conexión GPS y Sensores a la Placa CYD](diagrama_conexion_hardware.png) + +Para recibir datos de navegación y al mismo tiempo poder enviar comandos de suspensión y despertar, se utiliza la ficha de 4 pines del conector rotulado como **UART** (o **P1**) en la parte trasera de la placa CYD: + +| Pin del GPS N32+ | Pin en Conector UART (P1) | Función | Notas | +| :--- | :--- | :--- | :--- | +| **VCC / V+EN** (Naranja) | **Pin 1** (`Vin` 5V) | Alimentación | 5V directo del regulador/USB | +| **RX** (Blanco / Amarillo) | **Pin 2** (`GPIO 1` / TX0) | TX del ESP32 | Salida serial para enviar comandos UBX de reposo | +| **TX** (Azul / Verde) | **Pin 3** (`GPIO 3` / RX0) | RX del ESP32 | Entrada serial UART0 (sentencias NMEA a 9600 bps) | +| **GND** (Verde / Negro) | **Pin 4** (`GND`) | Masa común | Retorno de corriente | + +> **Programación por cable USB**: +> Dado que el conector UART comparte internamente las líneas GPIO 1 y GPIO 3 con el chip conversor USB-Serie (CH340), **desconectar la ficha de 4 pines del GPS al flashear nuevo firmware por cable USB** para evitar interferencias en la carga. Una vez completada la carga, reconectar la ficha para el funcionamiento normal. + +--- + +## 3. Comandos Binarios UBX de Reposo y Despertar + +El chip U-blox cuenta con el mensaje binario **`UBX-RXM-PMREQ`** (Class `0x02`, ID `0x41`), que ordena la desconexión inmediata de la etapa de radiofrecuencia y procesado satelital. + +### A. Trama para poner a dormir al GPS (Sleep / Backup Mode) +Estructura de 16 bytes con sincronismo, encabezado, longitud, payload y checksum Fletcher: + +```text +Header: 0xB5 0x62 (Sync UBX) +Class: 0x02 (RXM) +ID: 0x41 (PMREQ) +Length: 0x08 0x00 (8 bytes de payload) +Payload: 0x00 0x00 0x00 0x00 (Duración = 0: suspensión indefinida hasta despertar) + 0x02 0x00 0x00 0x00 (Flags: modo Backup / Software Standby) +Checksum: 0x4D 0x3B (CK_A, CK_B) +``` + +**Array en C++ listo para enviar**: +```cpp +const uint8_t UBX_SLEEP_CMD[] = { + 0xB5, 0x62, 0x02, 0x41, 0x08, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, + 0x4D, 0x3B +}; +``` + +### B. Secuencia para despertar al GPS (Wake-up) +U-blox admite el despertar por actividad en la línea serie (**Wake on UART**). Basta con transmitir una ráfaga corta de bytes (por ejemplo `0xFF`) para que el receptor reactive sus osciladores y vuelva al modo activo. + +```cpp +void gpsWake() { + // Envío de ráfaga de pulsos para despertar el receptor U-blox + uint8_t wakeBuffer[16]; + memset(wakeBuffer, 0xFF, sizeof(wakeBuffer)); + gpsSerial.write(wakeBuffer, sizeof(wakeBuffer)); + gpsSerial.flush(); +} +``` + +--- + +## 4. Código Modelo para Integrar en el Firmware + +```cpp +#include +#include + +#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. diff --git a/hardware/create_annotated_photo.py b/hardware/create_annotated_photo.py new file mode 100644 index 0000000..7d4a979 --- /dev/null +++ b/hardware/create_annotated_photo.py @@ -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) diff --git a/hardware/diagrama_conexion_ds18b20.png b/hardware/diagrama_conexion_ds18b20.png index f1ad8d4..383e374 100644 Binary files a/hardware/diagrama_conexion_ds18b20.png and b/hardware/diagrama_conexion_ds18b20.png differ diff --git a/hardware/diagrama_conexion_hardware.png b/hardware/diagrama_conexion_hardware.png new file mode 100644 index 0000000..383e374 Binary files /dev/null and b/hardware/diagrama_conexion_hardware.png differ diff --git a/hardware/generate_diagram.py b/hardware/generate_diagram.py new file mode 100644 index 0000000..5f650c8 --- /dev/null +++ b/hardware/generate_diagram.py @@ -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!") diff --git a/hardware/generate_gps_pinout.py b/hardware/generate_gps_pinout.py new file mode 100644 index 0000000..c9828d5 --- /dev/null +++ b/hardware/generate_gps_pinout.py @@ -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) diff --git a/hardware/gps_pinout.jpg b/hardware/gps_pinout.jpg new file mode 100644 index 0000000..3fac66c Binary files /dev/null and b/hardware/gps_pinout.jpg differ diff --git a/hardware/gps_pinout.png b/hardware/gps_pinout.png new file mode 100644 index 0000000..fff8c20 Binary files /dev/null and b/hardware/gps_pinout.png differ diff --git a/hardware/pinout_gps.png b/hardware/pinout_gps.png new file mode 100644 index 0000000..fff8c20 Binary files /dev/null and b/hardware/pinout_gps.png differ diff --git a/hardware/pinout_gps_foto_anotada.png b/hardware/pinout_gps_foto_anotada.png new file mode 100644 index 0000000..f32898c Binary files /dev/null and b/hardware/pinout_gps_foto_anotada.png differ diff --git a/pinout_gps.png b/pinout_gps.png new file mode 100644 index 0000000..fff8c20 Binary files /dev/null and b/pinout_gps.png differ diff --git a/pinout_gps_foto_anotada.png b/pinout_gps_foto_anotada.png new file mode 100644 index 0000000..f32898c Binary files /dev/null and b/pinout_gps_foto_anotada.png differ diff --git a/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_80.h b/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_80.h index bf92410..43bd934 100644 --- a/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_80.h +++ b/sketch/obd2_gauge/DSEG7_Modern_BoldItalic_80.h @@ -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, diff --git a/sketch/obd2_gauge/Latin_Sans_Bold_26.h b/sketch/obd2_gauge/Latin_Sans_Bold_26.h new file mode 100644 index 0000000..b595105 --- /dev/null +++ b/sketch/obd2_gauge/Latin_Sans_Bold_26.h @@ -0,0 +1,1293 @@ +#pragma once +#include + +// Antialiased Latin Sans Bold 26pt (VLW font for TFT_eSPI) +const uint8_t Latin_Sans_Bold_26[] PROGMEM = { + 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2D, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0D, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x09, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4B, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x4C, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, + 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x19, + 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0D, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x09, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x13, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6B, + 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x15, + 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x6E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x11, + 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x0E, + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x0F, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0D, + 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC9, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCD, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xD1, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD3, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDA, 0x00, 0x00, 0x00, 0x18, + 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xA0, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x17, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0F, + 0x3C, 0xFF, 0xFF, 0xFF, 0x70, 0x3C, 0xFF, 0xFF, 0xFF, 0x70, 0x3C, 0xFF, 0xFF, 0xFF, 0x70, 0x3C, + 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0x00, 0x24, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x53, 0xFF, + 0xFF, 0xB2, 0x00, 0x00, 0x00, 0x83, 0xFF, 0xFF, 0x83, 0x00, 0x00, 0x00, 0xB2, 0xFF, 0xFF, 0x55, + 0x00, 0x00, 0x00, 0xE2, 0xFF, 0xFF, 0x26, 0x00, 0x00, 0x11, 0xFF, 0xFF, 0xF5, 0x02, 0x00, 0x00, + 0x40, 0xFF, 0xFF, 0xCA, 0x00, 0x00, 0x00, 0x70, 0xFF, 0xFF, 0x9B, 0x00, 0x00, 0x00, 0x9F, 0xFF, + 0xFF, 0x6D, 0x00, 0x00, 0x00, 0xCF, 0xFF, 0xFF, 0x3E, 0x00, 0x00, 0x04, 0xF9, 0xFF, 0xFF, 0x10, + 0x00, 0x00, 0x2D, 0xFF, 0xFF, 0xE2, 0x00, 0x00, 0x00, 0x5D, 0xFF, 0xFF, 0xB3, 0x00, 0x00, 0x00, + 0x8C, 0xFF, 0xFF, 0x85, 0x00, 0x00, 0x00, 0xBC, 0xFF, 0xFF, 0x56, 0x00, 0x00, 0x00, 0xEB, 0xFF, + 0xFF, 0x28, 0x00, 0x00, 0x1A, 0xFF, 0xFF, 0xF7, 0x03, 0x00, 0x00, 0x4A, 0xFF, 0xFF, 0xCB, 0x00, + 0x00, 0x00, 0x79, 0xFF, 0xFF, 0x9D, 0x00, 0x00, 0x00, 0xA8, 0xFF, 0xFF, 0x6F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x67, 0xC5, 0xE7, 0xF8, 0xE0, 0x9C, 0x27, 0x00, 0x00, 0x00, 0x00, 0x11, 0xD0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x4A, 0x00, 0x00, 0x00, 0xA8, 0xFF, 0xFF, 0xFF, 0xEC, + 0xD9, 0xFC, 0xFF, 0xFF, 0xF2, 0x16, 0x00, 0x25, 0xFE, 0xFF, 0xFF, 0x99, 0x03, 0x00, 0x37, 0xF9, + 0xFF, 0xFF, 0x87, 0x00, 0x78, 0xFF, 0xFF, 0xFC, 0x10, 0x00, 0x00, 0x00, 0xA7, 0xFF, 0xFF, 0xDA, + 0x00, 0xB1, 0xFF, 0xFF, 0xCD, 0x00, 0x00, 0x00, 0x00, 0x69, 0xFF, 0xFF, 0xFF, 0x16, 0xD5, 0xFF, + 0xFF, 0xAF, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x3C, 0xEB, 0xFF, 0xFF, 0x9F, 0x00, + 0x00, 0x00, 0x00, 0x38, 0xFF, 0xFF, 0xFF, 0x52, 0xF5, 0xFF, 0xFF, 0x99, 0x00, 0x00, 0x00, 0x00, + 0x32, 0xFF, 0xFF, 0xFF, 0x5E, 0xF3, 0xFF, 0xFF, 0x99, 0x00, 0x00, 0x00, 0x00, 0x34, 0xFF, 0xFF, + 0xFF, 0x5D, 0xE7, 0xFF, 0xFF, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x3C, 0xFF, 0xFF, 0xFF, 0x52, 0xD0, + 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x4D, 0xFF, 0xFF, 0xFF, 0x39, 0xA7, 0xFF, 0xFF, 0xD1, + 0x00, 0x00, 0x00, 0x00, 0x6E, 0xFF, 0xFF, 0xFF, 0x10, 0x6A, 0xFF, 0xFF, 0xFD, 0x14, 0x00, 0x00, + 0x00, 0xAF, 0xFF, 0xFF, 0xCE, 0x00, 0x19, 0xF9, 0xFF, 0xFF, 0x9D, 0x04, 0x00, 0x46, 0xFD, 0xFF, + 0xFF, 0x75, 0x00, 0x00, 0x8E, 0xFF, 0xFF, 0xFF, 0xED, 0xDE, 0xFE, 0xFF, 0xFF, 0xE3, 0x0A, 0x00, + 0x00, 0x06, 0xBA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0x32, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x67, 0xC5, 0xEF, 0xF7, 0xDB, 0x8F, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x74, 0xF9, + 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0xBC, 0xFF, 0xFF, 0xFF, 0xFF, 0xA4, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0xED, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, + 0x00, 0x31, 0xFF, 0xFF, 0xF6, 0x6C, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x38, 0xFF, + 0xCA, 0x27, 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x33, 0x81, 0x04, 0x00, 0x00, + 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, + 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, + 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x3C, 0xA8, 0xA8, + 0xA8, 0xA8, 0xFA, 0xFF, 0xFF, 0xE0, 0xA8, 0xA8, 0xA8, 0x7B, 0x5C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBC, 0x5C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xBC, 0x00, 0x00, 0x00, 0x28, 0x96, 0xD8, 0xF5, 0xF9, 0xE6, 0xB4, 0x58, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x5F, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB6, 0x07, 0x00, + 0x00, 0x2E, 0xFB, 0xFF, 0xFF, 0xFF, 0xEF, 0xE4, 0xFE, 0xFF, 0xFF, 0xFF, 0x89, 0x00, 0x00, 0xA1, + 0xFF, 0xFF, 0xFF, 0x6B, 0x01, 0x00, 0x2E, 0xEF, 0xFF, 0xFF, 0xEC, 0x02, 0x00, 0xBA, 0xE4, 0xF1, + 0xC7, 0x00, 0x00, 0x00, 0x00, 0x95, 0xFF, 0xFF, 0xFF, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x8F, 0xFF, 0xFF, 0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x09, 0xE1, 0xFF, 0xFF, 0xE3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xA0, 0xFF, + 0xFF, 0xFF, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x9E, 0xFF, 0xFF, 0xFF, 0xCD, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xBB, 0xFF, 0xFF, 0xFF, 0xD1, 0x18, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0xD6, 0xFF, 0xFF, 0xFF, 0xC1, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x26, 0xE6, 0xFF, 0xFF, 0xFF, 0x92, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE4, + 0xFF, 0xFF, 0xFB, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xCB, 0xFF, 0xFF, 0xF9, + 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xF5, 0xFF, 0xFF, 0xFF, 0xF2, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0x65, 0x18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x6C, 0x18, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6C, + 0x00, 0x00, 0x00, 0x2B, 0x97, 0xD8, 0xF5, 0xF9, 0xE6, 0xB5, 0x5D, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x6E, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC2, 0x0E, 0x00, 0x00, 0x48, 0xFF, 0xFF, + 0xFF, 0xFF, 0xEB, 0xE8, 0xFF, 0xFF, 0xFF, 0xFF, 0x97, 0x00, 0x00, 0xC6, 0xFF, 0xFF, 0xF8, 0x4C, + 0x00, 0x00, 0x3A, 0xF3, 0xFF, 0xFF, 0xF1, 0x00, 0x05, 0xC8, 0xDD, 0xEF, 0x99, 0x00, 0x00, 0x00, + 0x00, 0x9F, 0xFF, 0xFF, 0xFE, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, + 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x20, 0x82, 0xFF, 0xFF, 0xFF, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0xFF, 0xFF, 0xFF, 0xFF, 0xEA, 0x63, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0xFF, 0xFF, 0xFF, 0xCE, 0x53, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xB6, 0xE4, 0xFB, 0xFF, 0xFF, 0xFF, 0xD5, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x4E, 0xE9, 0xFF, 0xFF, 0xE6, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x5B, 0xFF, 0xFF, 0xFF, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x2F, 0xFF, 0xFF, 0xFF, 0x7D, 0x3D, 0xC6, 0xDB, 0xF0, 0x66, 0x00, 0x00, 0x00, 0x00, 0x56, + 0xFF, 0xFF, 0xFF, 0x6F, 0x1C, 0xFC, 0xFF, 0xFF, 0xEA, 0x41, 0x00, 0x00, 0x31, 0xDD, 0xFF, 0xFF, + 0xFF, 0x38, 0x00, 0x9D, 0xFF, 0xFF, 0xFF, 0xFF, 0xF6, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0x00, + 0x00, 0x0A, 0xB2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD1, 0x19, 0x00, 0x00, 0x00, + 0x00, 0x4B, 0xA8, 0xDE, 0xF7, 0xFA, 0xE9, 0xBA, 0x66, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x5D, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x13, 0xEB, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xA1, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0xFF, + 0xFF, 0xDE, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xDB, 0xFF, 0xE9, + 0x7C, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0xFF, 0xFF, 0x61, 0x75, + 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xFA, 0xFF, 0xC0, 0x00, 0x78, 0xFF, + 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xC7, 0xFF, 0xF8, 0x27, 0x00, 0x78, 0xFF, 0xFF, + 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6B, 0xFF, 0xFF, 0x7E, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xF0, + 0x00, 0x00, 0x00, 0x00, 0x1B, 0xF1, 0xFF, 0xD7, 0x06, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xF0, 0x00, + 0x00, 0x00, 0x00, 0xAF, 0xFF, 0xFE, 0x3E, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xF0, 0x00, 0x00, + 0x00, 0x4F, 0xFF, 0xFF, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, + 0x9C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x9C, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x66, 0xA8, + 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xA8, 0xD1, 0xFF, 0xFF, 0xFA, 0xA8, 0xA8, 0x37, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB4, 0x00, 0x00, 0x28, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xB4, 0x00, 0x00, 0x37, 0xFF, 0xFF, 0xFF, 0xB7, 0xA8, 0xA8, 0xA8, 0xA8, + 0xA8, 0xA8, 0x76, 0x00, 0x00, 0x47, 0xFF, 0xFF, 0xFF, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x56, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x66, 0xFF, 0xFF, 0xFD, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, + 0xFF, 0xFF, 0xEE, 0x47, 0xBD, 0xF2, 0xF7, 0xD7, 0x8A, 0x15, 0x00, 0x00, 0x00, 0x85, 0xFF, 0xFF, + 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE5, 0x2B, 0x00, 0x00, 0x94, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF5, 0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xDA, 0x05, 0x00, 0xA4, 0xFF, 0xFF, 0xFC, 0x6B, 0x05, 0x00, + 0x2F, 0xD9, 0xFF, 0xFF, 0xFF, 0x59, 0x00, 0x05, 0x08, 0x08, 0x06, 0x00, 0x00, 0x00, 0x00, 0x3F, + 0xFF, 0xFF, 0xFF, 0x9A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xFB, 0xFF, + 0xFF, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xAD, + 0x15, 0xC2, 0xD7, 0xED, 0x90, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x87, 0x00, 0xDD, + 0xFF, 0xFF, 0xF8, 0x51, 0x00, 0x00, 0x2B, 0xD9, 0xFF, 0xFF, 0xFF, 0x35, 0x00, 0x5E, 0xFF, 0xFF, + 0xFF, 0xFF, 0xEA, 0xE4, 0xFF, 0xFF, 0xFF, 0xFF, 0xA5, 0x00, 0x00, 0x00, 0x84, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAE, 0x09, 0x00, 0x00, 0x00, 0x00, 0x36, 0x9F, 0xDB, 0xF7, 0xF9, + 0xE3, 0xAC, 0x4B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0xA7, 0xE4, 0xFA, 0xEE, 0xBC, + 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8D, + 0x00, 0x00, 0x00, 0x00, 0x60, 0xFF, 0xFF, 0xFF, 0xF7, 0xD9, 0xFA, 0xFF, 0xFF, 0xFF, 0x45, 0x00, + 0x00, 0x0B, 0xE9, 0xFF, 0xFF, 0xBD, 0x12, 0x00, 0x25, 0xEC, 0xFF, 0xFF, 0xB8, 0x00, 0x00, 0x59, + 0xFF, 0xFF, 0xF6, 0x17, 0x00, 0x00, 0x00, 0x7C, 0xE0, 0xB8, 0x8B, 0x00, 0x00, 0xA4, 0xFF, 0xFF, + 0xB4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD2, 0xFF, 0xFF, 0x86, 0x28, + 0xA6, 0xE9, 0xF9, 0xDE, 0x8F, 0x15, 0x00, 0x00, 0x00, 0xF3, 0xFF, 0xFF, 0xAA, 0xF6, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xE1, 0x1F, 0x00, 0x01, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0xAE, 0xE6, 0xFF, + 0xFF, 0xFF, 0xBD, 0x00, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x6B, 0x00, 0x00, 0x0E, 0xC8, 0xFF, 0xFF, + 0xFF, 0x2C, 0x00, 0xFC, 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x66, + 0x00, 0xEA, 0xFF, 0xFF, 0xBC, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xFF, 0xFF, 0xFF, 0x7D, 0x00, 0xBE, + 0xFF, 0xFF, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x23, 0xFF, 0xFF, 0xFF, 0x75, 0x00, 0x7F, 0xFF, 0xFF, + 0xFD, 0x24, 0x00, 0x00, 0x00, 0x5A, 0xFF, 0xFF, 0xFF, 0x53, 0x00, 0x24, 0xFC, 0xFF, 0xFF, 0xBA, + 0x0C, 0x00, 0x1F, 0xDB, 0xFF, 0xFF, 0xF7, 0x12, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0xF0, 0xD3, + 0xFA, 0xFF, 0xFF, 0xFF, 0x88, 0x00, 0x00, 0x00, 0x05, 0xAB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xAC, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0xB6, 0xE9, 0xFB, 0xEC, 0xB9, 0x57, 0x01, + 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x50, 0xE0, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x50, 0xD2, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFF, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x61, 0xFF, 0xFF, 0xDD, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xF0, 0xFF, + 0xFF, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0xFF, 0xFF, 0xAD, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xFF, 0xFF, 0xF6, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0E, 0xE6, 0xFF, 0xFF, 0x8B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, + 0xFF, 0xFF, 0xF2, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0xF7, 0xFF, 0xFF, 0x82, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0xFF, 0xFF, 0xF7, 0x11, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xED, 0xFF, 0xFF, 0xA5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x50, 0xFF, 0xFF, 0xFF, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA4, 0xFF, + 0xFF, 0xF8, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xCA, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xFF, 0xFF, 0xFF, 0x9E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x2F, 0xFF, 0xFF, 0xFF, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x40, 0xFF, 0xFF, 0xFF, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, + 0x9F, 0xDA, 0xF5, 0xFA, 0xEA, 0xBF, 0x70, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDC, 0x20, 0x00, 0x00, 0x45, 0xFF, 0xFF, 0xFF, 0xF1, 0x9B, 0x8B, + 0xCA, 0xFF, 0xFF, 0xFF, 0xBF, 0x00, 0x00, 0xA1, 0xFF, 0xFF, 0xFF, 0x34, 0x00, 0x00, 0x01, 0xC2, + 0xFF, 0xFF, 0xFF, 0x19, 0x00, 0xB6, 0xFF, 0xFF, 0xEE, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, + 0xFF, 0x2F, 0x00, 0x8E, 0xFF, 0xFF, 0xFB, 0x0A, 0x00, 0x00, 0x00, 0x93, 0xFF, 0xFF, 0xFB, 0x0C, + 0x00, 0x1F, 0xF5, 0xFF, 0xFF, 0xA2, 0x1B, 0x0A, 0x49, 0xF2, 0xFF, 0xFF, 0x8E, 0x00, 0x00, 0x00, + 0x39, 0xDC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF6, 0x8C, 0x03, 0x00, 0x00, 0x00, 0x08, 0x64, + 0xE6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA1, 0x34, 0x00, 0x00, 0x00, 0x14, 0xC9, 0xFF, 0xFF, 0xEF, + 0x9C, 0x8D, 0xC7, 0xFF, 0xFF, 0xFC, 0x6B, 0x00, 0x00, 0x9B, 0xFF, 0xFF, 0xF3, 0x20, 0x00, 0x00, + 0x00, 0x96, 0xFF, 0xFF, 0xFA, 0x26, 0x03, 0xF9, 0xFF, 0xFF, 0xA1, 0x00, 0x00, 0x00, 0x00, 0x27, + 0xFF, 0xFF, 0xFF, 0x7C, 0x22, 0xFF, 0xFF, 0xFF, 0x88, 0x00, 0x00, 0x00, 0x00, 0x10, 0xFF, 0xFF, + 0xFF, 0x9E, 0x19, 0xFF, 0xFF, 0xFF, 0xA4, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x93, + 0x00, 0xE0, 0xFF, 0xFF, 0xF4, 0x20, 0x00, 0x00, 0x00, 0x97, 0xFF, 0xFF, 0xFF, 0x5A, 0x00, 0x6B, + 0xFF, 0xFF, 0xFF, 0xF1, 0xA1, 0x8F, 0xC5, 0xFF, 0xFF, 0xFF, 0xDC, 0x07, 0x00, 0x00, 0x8F, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDE, 0x28, 0x00, 0x00, 0x00, 0x00, 0x39, 0x9E, 0xD9, + 0xF5, 0xFB, 0xEB, 0xBF, 0x70, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x91, 0xD5, 0xF3, 0xF7, + 0xDC, 0x97, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0xF6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xF7, 0x4D, 0x00, 0x00, 0x00, 0x2F, 0xF9, 0xFF, 0xFF, 0xFF, 0xE8, 0xE6, 0xFF, 0xFF, 0xFF, 0xF6, + 0x20, 0x00, 0x00, 0xA9, 0xFF, 0xFF, 0xFF, 0x6D, 0x00, 0x00, 0x5F, 0xFE, 0xFF, 0xFF, 0x9D, 0x00, + 0x00, 0xEF, 0xFF, 0xFF, 0xCA, 0x00, 0x00, 0x00, 0x00, 0xAE, 0xFF, 0xFF, 0xF0, 0x06, 0x0E, 0xFF, + 0xFF, 0xFF, 0x95, 0x00, 0x00, 0x00, 0x00, 0x66, 0xFF, 0xFF, 0xFF, 0x33, 0x10, 0xFF, 0xFF, 0xFF, + 0x92, 0x00, 0x00, 0x00, 0x00, 0x51, 0xFF, 0xFF, 0xFF, 0x5C, 0x01, 0xF4, 0xFF, 0xFF, 0xC2, 0x00, + 0x00, 0x00, 0x00, 0x7B, 0xFF, 0xFF, 0xFF, 0x6F, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x51, 0x00, 0x00, + 0x29, 0xED, 0xFF, 0xFF, 0xFF, 0x7B, 0x00, 0x49, 0xFF, 0xFF, 0xFF, 0xFD, 0xC5, 0xBC, 0xF7, 0xFF, + 0xFF, 0xFF, 0xFF, 0x75, 0x00, 0x00, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, + 0xFF, 0x68, 0x00, 0x00, 0x00, 0x5A, 0xC5, 0xF4, 0xF3, 0xC7, 0x60, 0x22, 0xFF, 0xFF, 0xFF, 0x49, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0xFF, 0xFF, 0xFF, 0x1C, 0x00, 0x65, + 0xB0, 0xD6, 0xB8, 0x00, 0x00, 0x00, 0x00, 0xAE, 0xFF, 0xFF, 0xD1, 0x00, 0x00, 0x78, 0xFF, 0xFF, + 0xFF, 0x5A, 0x00, 0x01, 0x6C, 0xFF, 0xFF, 0xFF, 0x6D, 0x00, 0x00, 0x14, 0xF0, 0xFF, 0xFF, 0xFF, + 0xE0, 0xE8, 0xFF, 0xFF, 0xFF, 0xD2, 0x05, 0x00, 0x00, 0x00, 0x50, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xD7, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xA4, 0xE3, 0xFA, 0xEF, 0xC9, 0x73, + 0x07, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x28, 0x80, 0xFF, 0xFF, 0xFF, 0x28, 0x80, 0xFF, + 0xFF, 0xFF, 0x28, 0x80, 0xFF, 0xFF, 0xFF, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x28, 0x80, 0xFF, 0xFF, 0xFF, 0x28, + 0x80, 0xFF, 0xFF, 0xFF, 0x28, 0x80, 0xFF, 0xFF, 0xFF, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0xF5, 0xFF, 0xFF, 0xFF, 0xC5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0xFD, 0xFF, 0xFF, 0xA7, 0xFF, 0xFF, 0xDA, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xFF, 0xFF, 0xE8, 0x28, + 0xFF, 0xFF, 0xFF, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD3, 0xFF, + 0xFF, 0x99, 0x00, 0xD8, 0xFF, 0xFF, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x30, 0xFF, 0xFF, 0xFF, 0x47, 0x00, 0x86, 0xFF, 0xFF, 0xEB, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x8D, 0xFF, 0xFF, 0xEE, 0x05, 0x00, 0x32, 0xFF, 0xFF, 0xFF, 0x4D, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE6, 0xFF, 0xFF, 0xA0, 0x00, 0x00, 0x00, 0xDD, 0xFF, 0xFF, + 0xA9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0xFF, 0xFF, 0xFF, 0x4C, 0x00, 0x00, 0x00, + 0x8A, 0xFF, 0xFF, 0xF7, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA3, 0xFF, 0xFF, 0xF2, 0x07, + 0x00, 0x00, 0x00, 0x36, 0xFF, 0xFF, 0xFF, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0xF4, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x5D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1D, 0x00, + 0x00, 0x00, 0x00, 0xBA, 0xFF, 0xFF, 0xFD, 0xD0, 0xD0, 0xD0, 0xD0, 0xD0, 0xD0, 0xD8, 0xFF, 0xFF, + 0xFF, 0x77, 0x00, 0x00, 0x00, 0x1A, 0xFD, 0xFF, 0xFF, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0xEB, 0xFF, 0xFF, 0xD4, 0x00, 0x00, 0x00, 0x74, 0xFF, 0xFF, 0xFF, 0x57, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, 0xD1, 0xFF, 0xFF, 0xEE, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0xFF, 0xFF, 0xFF, 0x8D, 0x00, 0x2D, 0xFF, 0xFF, + 0xFF, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCD, 0xFF, 0xFF, 0xE6, 0x03, + 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xE9, 0xC3, 0x81, 0x1C, 0x00, 0x00, + 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x59, + 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xE3, 0xC8, 0xC8, 0xC8, 0xC8, 0xDA, 0xFA, 0xFF, 0xFF, 0xFF, + 0xF8, 0x22, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0xBD, 0xFF, + 0xFF, 0xFF, 0x73, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, + 0xFF, 0xFF, 0xFF, 0x86, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xFF, 0xFF, 0xFF, 0x60, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x11, + 0x47, 0xDD, 0xFF, 0xFF, 0xE3, 0x0A, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xC5, 0x22, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0x6E, 0x12, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xE1, 0xC4, 0xC4, + 0xC4, 0xC4, 0xCC, 0xE7, 0xFF, 0xFF, 0xFF, 0xEE, 0x43, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3D, 0xE6, 0xFF, 0xFF, 0xF1, 0x16, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xFF, 0x6E, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0xFF, 0xFF, 0xFF, 0x91, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0xFF, 0xFF, 0xFF, 0x83, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A, 0xE5, 0xFF, 0xFF, 0xFF, 0x48, 0x44, + 0xFF, 0xFF, 0xFF, 0xE3, 0xC8, 0xC8, 0xC8, 0xC8, 0xCD, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0x02, + 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC4, 0x15, + 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF5, 0xDC, 0xA8, 0x51, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x83, 0xC2, 0xE5, 0xF8, 0xF7, 0xE4, 0xB8, 0x6F, 0x11, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x98, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xEF, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x06, 0xC4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFD, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x68, 0x00, 0x00, 0x00, 0x91, 0xFF, 0xFF, 0xFF, 0xF5, 0x7E, 0x24, + 0x01, 0x07, 0x2F, 0x9A, 0xFE, 0xFF, 0xFF, 0xF9, 0x26, 0x00, 0x1B, 0xFA, 0xFF, 0xFF, 0xF8, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F, 0xFF, 0xFF, 0xFF, 0x9B, 0x00, 0x75, 0xFF, 0xFF, 0xFF, + 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB0, 0xBB, 0x74, 0x2A, 0x00, 0xB3, 0xFF, + 0xFF, 0xFF, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xD7, 0xFF, 0xFF, 0xF2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xE8, 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD3, 0xFF, 0xFF, 0xF5, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA9, 0xFF, 0xFF, 0xFF, 0x2F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0xFF, 0xFF, 0xFF, 0x93, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xBF, 0x65, 0x11, 0x00, 0x0E, 0xF0, 0xFF, 0xFF, + 0xFC, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0xF6, 0xFF, 0xFF, 0xD7, 0x01, 0x00, 0x73, + 0xFF, 0xFF, 0xFF, 0xF9, 0x88, 0x29, 0x02, 0x03, 0x23, 0x82, 0xF6, 0xFF, 0xFF, 0xFF, 0x53, 0x00, + 0x00, 0x00, 0xA4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8C, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x7A, 0xF5, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF5, + 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x74, 0xB7, 0xE2, 0xF5, 0xFA, 0xE8, 0xBD, + 0x75, 0x15, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFA, 0xE9, 0xCF, + 0x97, 0x4B, 0x01, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xD8, 0x3B, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xEB, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x5E, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, + 0x00, 0x08, 0x44, 0xBB, 0xFF, 0xFF, 0xFF, 0xFA, 0x38, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x8E, 0xFF, 0xFF, 0xFF, 0xC3, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xD1, 0xFF, 0xFF, 0xFF, 0x2C, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6A, 0xFF, 0xFF, 0xFF, 0x6F, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xFF, 0xFF, 0xFF, 0x94, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0xFF, 0xFF, 0xFF, 0xA8, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xA7, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0xFF, 0xFF, 0xFF, + 0x91, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xFF, 0xFF, + 0xFF, 0x64, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xDE, 0xFF, + 0xFF, 0xFD, 0x1C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x99, 0xFF, + 0xFF, 0xFF, 0xAF, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x01, 0x35, 0xB4, 0xFF, + 0xFF, 0xFF, 0xEE, 0x1D, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE5, 0xF6, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF7, 0x4E, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xDC, 0x32, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, + 0xE1, 0xAF, 0x5E, 0x06, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD0, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD0, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, + 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xB9, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, + 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0x07, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, + 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0x4A, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF0, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, + 0xE4, 0xD5, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA4, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA4, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, + 0xE4, 0xE4, 0xE4, 0x92, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x75, 0xB4, 0xE1, 0xF3, 0xFB, 0xED, 0xC9, + 0x8E, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x82, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xA8, 0x0F, 0x00, 0x00, 0x00, 0x02, 0xB1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCA, 0x0A, 0x00, 0x00, 0x83, 0xFF, 0xFF, 0xFF, 0xFA, + 0x8C, 0x2D, 0x04, 0x02, 0x21, 0x7F, 0xF4, 0xFF, 0xFF, 0xFF, 0x8E, 0x00, 0x17, 0xF6, 0xFF, 0xFF, + 0xFC, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0xF3, 0xFF, 0xFF, 0xF8, 0x12, 0x71, 0xFF, + 0xFF, 0xFF, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0x75, 0x24, 0x00, + 0xB1, 0xFF, 0xFF, 0xFF, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xD6, 0xFF, 0xFF, 0xF3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xE8, 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0xFF, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0xD3, 0xFF, 0xFF, 0xF6, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0xAA, 0xFF, 0xFF, 0xFF, 0x35, 0x00, + 0x00, 0x00, 0x00, 0x24, 0xC4, 0xC4, 0xC4, 0xC4, 0xF4, 0xFF, 0xFF, 0x9C, 0x69, 0xFF, 0xFF, 0xFF, + 0x9F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0xFF, 0xFF, 0x9C, 0x10, 0xF2, + 0xFF, 0xFF, 0xFE, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xD6, 0xFF, 0xFF, 0x9C, + 0x00, 0x78, 0xFF, 0xFF, 0xFF, 0xFD, 0x94, 0x2E, 0x02, 0x00, 0x05, 0x2D, 0x6C, 0xCF, 0xFF, 0xFF, + 0xFF, 0x9C, 0x00, 0x01, 0xAB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xF4, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xDE, 0x34, 0x00, 0x00, 0x02, 0x7E, 0xF6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xEE, 0x87, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x76, 0xB7, 0xE2, 0xF5, 0xF8, + 0xE5, 0xCC, 0x95, 0x55, 0x0A, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, + 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x88, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xBE, 0xFF, + 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, + 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9C, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x92, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0C, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, + 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0xFF, 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xFF, + 0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0xFF, 0xB0, + 0x43, 0x98, 0xBF, 0xE7, 0x39, 0x00, 0x00, 0x00, 0x15, 0xFF, 0xFF, 0xFF, 0xA8, 0x65, 0xFF, 0xFF, + 0xFF, 0x88, 0x00, 0x00, 0x00, 0x42, 0xFF, 0xFF, 0xFF, 0x8D, 0x17, 0xFB, 0xFF, 0xFF, 0xF5, 0x4C, + 0x02, 0x20, 0xCC, 0xFF, 0xFF, 0xFF, 0x4B, 0x00, 0x97, 0xFF, 0xFF, 0xFF, 0xFF, 0xFA, 0xFF, 0xFF, + 0xFF, 0xFF, 0xCE, 0x02, 0x00, 0x0B, 0xC2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDB, 0x21, + 0x00, 0x00, 0x00, 0x03, 0x65, 0xBD, 0xEC, 0xFC, 0xF1, 0xC9, 0x77, 0x0D, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0xD9, 0xFF, 0xFF, 0xFE, 0x68, 0x00, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xD7, 0xFF, 0xFF, 0xFE, 0x67, + 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x16, 0xD6, 0xFF, 0xFF, 0xFE, + 0x66, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x15, 0xD4, 0xFF, 0xFF, + 0xFE, 0x65, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x14, 0xD2, 0xFF, + 0xFF, 0xFE, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x13, 0xD0, + 0xFF, 0xFF, 0xFE, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x12, + 0xCE, 0xFF, 0xFF, 0xFE, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, + 0x8C, 0xCD, 0xFF, 0xFF, 0xFF, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xE2, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0xFF, 0xFF, 0xFF, 0xC0, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xA6, 0x0D, 0xB5, 0xFF, 0xFF, 0xFF, 0x90, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x9B, 0x00, 0x00, 0x11, 0xDE, 0xFF, 0xFF, 0xFF, + 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x31, 0xF7, + 0xFF, 0xFF, 0xF6, 0x31, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x61, 0xFF, 0xFF, 0xFF, 0xDF, 0x13, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x9A, 0xFF, 0xFF, 0xFF, 0xBB, 0x03, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xCB, 0xFF, 0xFF, 0xFF, 0x8A, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xEC, 0xFF, 0xFF, 0xFF, 0x56, 0x00, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xFD, 0xFF, 0xFF, + 0xF4, 0x2D, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, + 0xE4, 0x11, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x14, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x14, + 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xE6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0xFF, 0xFF, + 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8B, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xD8, 0xFF, 0xFF, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xF4, 0xF7, 0xFF, 0xDC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xFF, 0xFF, 0xEC, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xF8, + 0xB8, 0xFF, 0xFF, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xB2, 0xFF, 0xFF, 0xEC, 0x44, + 0xFF, 0xFF, 0xFF, 0x73, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0xC8, 0xFF, 0xFF, 0x78, 0xFF, + 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xD1, 0x00, 0x00, 0x00, 0x19, 0xFE, 0xFF, + 0xD0, 0x67, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0xDE, 0xFF, 0xFF, 0x22, 0x00, 0x00, + 0x68, 0xFF, 0xFF, 0x87, 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x90, 0xFF, 0xFF, + 0x74, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0x37, 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, + 0x3E, 0xFF, 0xFF, 0xC5, 0x00, 0x0E, 0xF9, 0xFF, 0xE4, 0x01, 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, + 0xFF, 0xFF, 0x10, 0x02, 0xE8, 0xFF, 0xFE, 0x16, 0x56, 0xFF, 0xFF, 0x92, 0x00, 0x68, 0xFF, 0xFF, + 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x99, 0xFF, 0xFF, 0x59, 0x9E, 0xFF, 0xFF, 0x40, 0x00, + 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x46, 0xFF, 0xFF, 0x9A, 0xE4, 0xFF, + 0xEA, 0x03, 0x00, 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x05, 0xEE, 0xFF, + 0xF2, 0xFF, 0xFF, 0x9B, 0x00, 0x00, 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, + 0x00, 0xA1, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, 0x00, 0x00, 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, + 0xFF, 0x10, 0x00, 0x00, 0x4F, 0xFF, 0xFF, 0xFF, 0xF0, 0x05, 0x00, 0x00, 0x68, 0xFF, 0xFF, 0xEC, + 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x08, 0xF3, 0xFF, 0xFF, 0xA3, 0x00, 0x00, 0x00, 0x68, + 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0xA9, 0xFF, 0xFF, 0x51, 0x00, + 0x00, 0x00, 0x68, 0xFF, 0xFF, 0xEC, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0x4B, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xD7, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6D, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, + 0x11, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xEB, 0xE8, 0xFF, + 0xFF, 0x8F, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xF6, 0x65, + 0xFF, 0xFF, 0xFA, 0x26, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, + 0x09, 0xD2, 0xFF, 0xFF, 0xB1, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, + 0xFF, 0x0F, 0x45, 0xFF, 0xFF, 0xFF, 0x42, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, + 0xFF, 0xFF, 0x10, 0x00, 0xB6, 0xFF, 0xFF, 0xD0, 0x02, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, + 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x2A, 0xFC, 0xFF, 0xFF, 0x64, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, + 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x96, 0xFF, 0xFF, 0xE7, 0x0C, 0x4A, 0xFF, 0xFF, 0xFF, + 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x15, 0xF1, 0xFF, 0xFF, 0x85, 0x44, 0xFF, 0xFF, + 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x77, 0xFF, 0xFF, 0xF7, 0x55, 0xFF, + 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xC6, + 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x57, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC7, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x39, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xA8, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x00, 0x00, 0x00, 0x00, 0x13, 0x72, 0xB5, 0xE1, + 0xF4, 0xF7, 0xE4, 0xBE, 0x7E, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x77, 0xF4, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFA, 0x8F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xA4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x07, 0x00, + 0x00, 0x00, 0x75, 0xFF, 0xFF, 0xFF, 0xFA, 0x8D, 0x2B, 0x03, 0x01, 0x21, 0x7A, 0xF3, 0xFF, 0xFF, + 0xFF, 0x9B, 0x00, 0x00, 0x11, 0xF1, 0xFF, 0xFF, 0xFD, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x33, 0xF4, 0xFF, 0xFF, 0xFE, 0x2E, 0x00, 0x6C, 0xFF, 0xFF, 0xFF, 0x95, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x72, 0xFF, 0xFF, 0xFF, 0x98, 0x00, 0xAD, 0xFF, 0xFF, 0xFF, 0x2E, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0xFA, 0xFF, 0xFF, 0xDA, 0x00, 0xD5, 0xFF, 0xFF, + 0xF3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCF, 0xFF, 0xFF, 0xFE, 0x04, + 0xE8, 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB7, 0xFF, + 0xFF, 0xFF, 0x17, 0xE5, 0xFF, 0xFF, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xBA, 0xFF, 0xFF, 0xFF, 0x17, 0xD2, 0xFF, 0xFF, 0xF7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xD2, 0xFF, 0xFF, 0xFC, 0x06, 0xA5, 0xFF, 0xFF, 0xFF, 0x39, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xFC, 0xFF, 0xFF, 0xD4, 0x00, 0x61, 0xFF, 0xFF, 0xFF, + 0xA6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x74, 0xFF, 0xFF, 0xFF, 0x89, 0x00, 0x0A, + 0xEA, 0xFF, 0xFF, 0xFF, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0xF4, 0xFF, 0xFF, 0xFA, + 0x21, 0x00, 0x00, 0x63, 0xFF, 0xFF, 0xFF, 0xFD, 0x97, 0x2E, 0x03, 0x01, 0x20, 0x7A, 0xF3, 0xFF, + 0xFF, 0xFF, 0x74, 0x00, 0x00, 0x00, 0x00, 0x91, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFB, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xA8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6B, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x6F, 0xB5, 0xE2, 0xF5, 0xFA, 0xE7, 0xBD, 0x77, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xD9, 0x9F, 0x3C, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9A, 0x02, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xF3, 0xE8, 0xE8, 0xE8, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x84, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x7A, 0xFE, 0xFF, 0xFF, 0xF7, 0x10, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0xFF, 0xFF, 0xFF, 0x4F, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0xFF, 0xFF, 0xFF, 0x6B, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0xFF, 0xFF, 0xFF, 0x60, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAB, 0xFF, 0xFF, 0xFF, 0x3B, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x7D, 0xFF, 0xFF, 0xFF, 0xDF, 0x01, 0x44, 0xFF, + 0xFF, 0xFF, 0xEF, 0xE0, 0xE0, 0xE0, 0xE5, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x52, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0x6E, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xD6, 0x90, 0x29, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x12, 0x71, 0xB4, 0xE1, 0xF4, 0xF7, 0xE4, 0xBE, 0x7D, 0x1C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x74, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0x8B, + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFB, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xBB, 0x05, 0x00, 0x00, 0x00, 0x71, 0xFF, 0xFF, 0xFF, 0xFB, 0x8E, 0x2C, 0x03, + 0x01, 0x21, 0x7B, 0xF4, 0xFF, 0xFF, 0xFF, 0x92, 0x00, 0x00, 0x0F, 0xF0, 0xFF, 0xFF, 0xFD, 0x4D, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0xF5, 0xFF, 0xFF, 0xFD, 0x26, 0x00, 0x69, 0xFF, 0xFF, + 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xFF, 0xFF, 0xFF, 0x91, 0x00, + 0xAB, 0xFF, 0xFF, 0xFF, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFB, 0xFF, + 0xFF, 0xD4, 0x00, 0xD4, 0xFF, 0xFF, 0xF4, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xD1, 0xFF, 0xFF, 0xFE, 0x03, 0xE7, 0xFF, 0xFF, 0xDF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x14, 0xE7, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xFF, 0x18, 0xD5, 0xFF, 0xFF, 0xF5, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCE, 0xFF, 0xFF, 0xFF, 0x07, 0xAB, + 0xFF, 0xFF, 0xFF, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0xFA, 0xFF, 0xFF, + 0xDC, 0x00, 0x6B, 0xFF, 0xFF, 0xFF, 0x97, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, + 0xFF, 0xFF, 0xFF, 0x9C, 0x00, 0x11, 0xF5, 0xFF, 0xFF, 0xFD, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1F, 0xEB, 0xFF, 0xFF, 0xFF, 0x38, 0x00, 0x00, 0x79, 0xFF, 0xFF, 0xFF, 0xF6, 0x74, 0x0F, + 0x00, 0x00, 0x07, 0x57, 0xE5, 0xFF, 0xFF, 0xFF, 0xAC, 0x00, 0x00, 0x00, 0x03, 0xAF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFB, 0xDC, 0xD8, 0xF5, 0xFF, 0xFF, 0xFF, 0xFF, 0xD9, 0x13, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x8B, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x19, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x87, 0xCC, 0xF5, 0xFF, 0xFF, 0xFF, 0xF6, 0xB6, 0x52, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0xFF, 0xFF, 0xFF, 0xBD, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBE, + 0xFF, 0xFF, 0xFF, 0x86, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x39, 0xFD, 0xFF, 0xFF, 0xFF, 0xF5, 0xD6, 0xD8, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x5F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xB7, 0xEF, 0xFB, 0xED, 0xCA, + 0x3E, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xD9, 0xA2, + 0x44, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xAA, 0x07, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF3, 0xE8, 0xE8, 0xE8, 0xE8, + 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0E, 0x72, 0xFD, 0xFF, 0xFF, 0xFC, 0x14, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1, 0xFF, 0xFF, 0xFF, 0x49, 0x00, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xFF, 0x55, 0x00, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xFF, 0x35, + 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x7B, 0xFF, 0xFF, 0xFF, + 0xDC, 0x02, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xF3, 0xE8, 0xE8, 0xE8, 0xE8, 0xED, 0xFF, 0xFF, 0xFF, + 0xFF, 0xF7, 0x42, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xD6, 0x3C, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xA9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, + 0x00, 0x03, 0xD1, 0xFF, 0xFF, 0xFB, 0x2F, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x00, 0x00, 0x00, 0x00, 0x3D, 0xFF, 0xFF, 0xFF, 0xC9, 0x02, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xFF, 0xFF, 0xFF, 0x6C, 0x00, 0x00, 0x00, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0xF2, 0xFF, 0xFF, 0xF1, 0x1B, 0x00, + 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xFF, 0xFF, 0xFF, + 0xAE, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xD7, + 0xFF, 0xFF, 0xFF, 0x4E, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xE2, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x4A, 0x9F, 0xD2, 0xEF, 0xFB, + 0xF6, 0xE5, 0xBE, 0x7A, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF2, 0x4A, 0x00, 0x00, 0x00, 0x00, 0xBE, 0xFF, 0xFF, 0xFF, 0xF6, + 0xC1, 0xAE, 0xC0, 0xF2, 0xFF, 0xFF, 0xFF, 0xF4, 0x20, 0x00, 0x00, 0x31, 0xFF, 0xFF, 0xFF, 0xBB, + 0x0D, 0x00, 0x00, 0x00, 0x0E, 0xA9, 0xFF, 0xFF, 0xFF, 0x8F, 0x00, 0x00, 0x5D, 0xFF, 0xFF, 0xFF, + 0x4B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xE6, 0xD3, 0xB0, 0x72, 0x00, 0x00, 0x51, 0xFF, 0xFF, + 0xFF, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xFE, + 0xFF, 0xFF, 0xFB, 0x8C, 0x2E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x9E, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEB, 0xB2, 0x7A, 0x3C, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x05, 0x98, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x90, 0x16, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x24, 0x84, 0xC9, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE8, 0x2B, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x3F, 0x78, 0xBD, 0xFD, 0xFF, 0xFF, 0xFF, + 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2D, 0xDF, 0xFF, + 0xFF, 0xFF, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, + 0xFF, 0xFF, 0xFF, 0x4D, 0x15, 0x90, 0xB7, 0xDD, 0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x75, 0xFF, 0xFF, 0xFF, 0x3E, 0x03, 0xE2, 0xFF, 0xFF, 0xFF, 0x92, 0x1E, 0x00, 0x00, 0x00, 0x06, + 0x55, 0xF0, 0xFF, 0xFF, 0xF5, 0x0B, 0x00, 0x4E, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xE8, 0xD6, 0xDE, + 0xF6, 0xFF, 0xFF, 0xFF, 0xFF, 0x7B, 0x00, 0x00, 0x00, 0x56, 0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFA, 0x81, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x6A, 0xB0, 0xDB, 0xF4, + 0xFD, 0xF6, 0xE5, 0xBD, 0x7E, 0x22, 0x00, 0x00, 0x00, 0xB4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0xB4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0xA0, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xFE, + 0xFF, 0xFF, 0xFA, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0x8A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, + 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, + 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, + 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, + 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, + 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, + 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, + 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x91, + 0xFF, 0xFF, 0xFF, 0x2F, 0x68, 0xFF, 0xFF, 0xFF, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xA1, 0xFF, 0xFF, 0xFF, 0x22, 0x51, 0xFF, 0xFF, 0xFF, 0x8B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xD8, 0xFF, 0xFF, 0xFC, 0x0A, 0x19, 0xFF, 0xFF, 0xFF, 0xE9, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x57, 0xFF, 0xFF, 0xFF, 0xC5, 0x00, 0x00, 0xB6, 0xFF, 0xFF, 0xFF, 0xD0, 0x42, 0x04, 0x00, + 0x15, 0x75, 0xF8, 0xFF, 0xFF, 0xFF, 0x55, 0x00, 0x00, 0x28, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, + 0xF5, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA6, 0x00, 0x00, 0x00, 0x00, 0x36, 0xDE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x8D, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x60, 0xAF, + 0xE1, 0xF6, 0xFA, 0xEA, 0xC5, 0x87, 0x24, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xFF, 0xFF, 0xFF, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xFF, 0xFF, 0xF2, 0x0A, 0x47, 0xFF, 0xFF, + 0xFF, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0xFF, 0xFF, 0xFF, 0x9F, 0x00, 0x03, + 0xE5, 0xFF, 0xFF, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x41, + 0x00, 0x00, 0x8B, 0xFF, 0xFF, 0xFF, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0xFF, 0xFF, + 0xE2, 0x01, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xFF, + 0xFF, 0xFF, 0x86, 0x00, 0x00, 0x00, 0x00, 0xCE, 0xFF, 0xFF, 0xD6, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x73, 0xFF, 0xFF, 0xFF, 0x29, 0x00, 0x00, 0x00, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0x27, 0x00, 0x00, + 0x00, 0x00, 0xC4, 0xFF, 0xFF, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xFB, 0xFF, 0xFF, 0x78, + 0x00, 0x00, 0x00, 0x17, 0xFD, 0xFF, 0xFF, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0xFF, + 0xFF, 0xC9, 0x00, 0x00, 0x00, 0x66, 0xFF, 0xFF, 0xFB, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x55, 0xFF, 0xFF, 0xFE, 0x1C, 0x00, 0x00, 0xB7, 0xFF, 0xFF, 0xB2, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xEF, 0xFF, 0xFF, 0x6C, 0x00, 0x0F, 0xF9, 0xFF, 0xFF, 0x55, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x99, 0xFF, 0xFF, 0xBC, 0x00, 0x59, 0xFF, 0xFF, 0xEF, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3B, 0xFF, 0xFF, 0xFA, 0x0D, 0xA3, 0xFF, 0xFF, 0x9A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDB, 0xFF, 0xFF, 0x50, 0xEA, 0xFF, + 0xFF, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xFF, 0xFF, 0xC1, + 0xFF, 0xFF, 0xDE, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xFE, + 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC2, 0xFF, 0xFF, 0xFF, 0xFF, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x63, 0xFF, 0xFF, 0xFF, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8, + 0xFF, 0xFF, 0xEE, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0xFF, 0xFF, 0x85, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x64, 0xFF, 0xFF, 0xFF, 0x62, 0x96, 0xFF, 0xFF, 0xFF, 0x29, 0x00, 0x00, 0x00, + 0x00, 0x21, 0xFF, 0xFF, 0xFF, 0xFF, 0xC2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9B, 0xFF, 0xFF, 0xFF, + 0x21, 0x54, 0xFF, 0xFF, 0xFF, 0x62, 0x00, 0x00, 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, + 0x07, 0x00, 0x00, 0x00, 0x00, 0xD2, 0xFF, 0xFF, 0xDF, 0x00, 0x14, 0xFE, 0xFF, 0xFF, 0x9B, 0x00, + 0x00, 0x00, 0x00, 0x98, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0x3B, 0x00, 0x00, 0x00, 0x0C, 0xFC, 0xFF, + 0xFF, 0x9D, 0x00, 0x00, 0xD1, 0xFF, 0xFF, 0xD4, 0x00, 0x00, 0x00, 0x00, 0xD3, 0xFF, 0xFF, 0xB3, + 0xFF, 0xFF, 0x77, 0x00, 0x00, 0x00, 0x40, 0xFF, 0xFF, 0xFF, 0x5B, 0x00, 0x00, 0x90, 0xFF, 0xFF, + 0xFE, 0x0F, 0x00, 0x00, 0x0F, 0xFD, 0xFF, 0xEE, 0x60, 0xFF, 0xFF, 0xB4, 0x00, 0x00, 0x00, 0x77, + 0xFF, 0xFF, 0xFF, 0x1A, 0x00, 0x00, 0x4E, 0xFF, 0xFF, 0xFF, 0x46, 0x00, 0x00, 0x49, 0xFF, 0xFF, + 0xB9, 0x2E, 0xFF, 0xFF, 0xEE, 0x01, 0x00, 0x00, 0xAE, 0xFF, 0xFF, 0xD8, 0x00, 0x00, 0x00, 0x0F, + 0xFC, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x83, 0xFF, 0xFF, 0x80, 0x04, 0xF6, 0xFF, 0xFF, 0x2D, 0x00, + 0x00, 0xE5, 0xFF, 0xFF, 0x96, 0x00, 0x00, 0x00, 0x00, 0xCB, 0xFF, 0xFF, 0xB8, 0x00, 0x00, 0xBD, + 0xFF, 0xFF, 0x45, 0x00, 0xC1, 0xFF, 0xFF, 0x69, 0x00, 0x1C, 0xFF, 0xFF, 0xFF, 0x54, 0x00, 0x00, + 0x00, 0x00, 0x8A, 0xFF, 0xFF, 0xEF, 0x01, 0x03, 0xF4, 0xFF, 0xFD, 0x0D, 0x00, 0x86, 0xFF, 0xFF, + 0xA5, 0x00, 0x53, 0xFF, 0xFF, 0xFE, 0x14, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x2A, + 0x31, 0xFF, 0xFF, 0xCF, 0x00, 0x00, 0x4A, 0xFF, 0xFF, 0xDD, 0x00, 0x8B, 0xFF, 0xFF, 0xD1, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0B, 0xFA, 0xFF, 0xFF, 0x63, 0x66, 0xFF, 0xFF, 0x94, 0x00, 0x00, 0x10, + 0xFE, 0xFF, 0xFF, 0x0F, 0xC6, 0xFF, 0xFF, 0x8F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC5, 0xFF, + 0xFF, 0x98, 0x9B, 0xFF, 0xFF, 0x59, 0x00, 0x00, 0x00, 0xD3, 0xFF, 0xFF, 0x44, 0xF9, 0xFF, 0xFF, + 0x4D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0xFF, 0xFF, 0xCD, 0xCF, 0xFF, 0xFF, 0x1D, 0x00, + 0x00, 0x00, 0x98, 0xFF, 0xFF, 0xA2, 0xFF, 0xFF, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x42, 0xFF, 0xFF, 0xFA, 0xFA, 0xFF, 0xE2, 0x00, 0x00, 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xF5, 0xFF, + 0xFF, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xA7, + 0x00, 0x00, 0x00, 0x00, 0x21, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE5, 0xFF, + 0xFF, 0xFF, 0xFF, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7D, 0xFF, 0xFF, 0xFF, + 0xFF, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xFF, 0xFA, 0x0A, 0x00, 0x00, 0x00, + 0x00, 0x0C, 0xDF, 0xFF, 0xFF, 0xED, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA9, 0xFF, 0xFF, + 0xFF, 0x46, 0x00, 0x00, 0x42, 0xFE, 0xFF, 0xFF, 0xA3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, + 0xFF, 0xFF, 0xA2, 0x00, 0x00, 0x00, 0x00, 0x97, 0xFF, 0xFF, 0xFF, 0x43, 0x00, 0x00, 0x00, 0x0A, + 0xDE, 0xFF, 0xFF, 0xEB, 0x14, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xE0, 0xFF, 0xFF, 0xDA, 0x08, 0x00, + 0x00, 0x8A, 0xFF, 0xFF, 0xFF, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0xFF, 0xFF, 0xFF, + 0x82, 0x00, 0x2E, 0xFB, 0xFF, 0xFF, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, + 0xFF, 0xFF, 0xF9, 0x2A, 0xC7, 0xFF, 0xFF, 0xF5, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0D, 0xE0, 0xFF, 0xFF, 0xEB, 0xFF, 0xFF, 0xFF, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCC, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBE, 0xFF, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xC2, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xD6, 0xFF, 0xFF, 0xFB, 0xFF, 0xFF, + 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0xFF, 0xFF, 0xFF, 0x4D, + 0xDC, 0xFF, 0xFF, 0xEE, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFB, 0xFF, 0xFF, + 0xA3, 0x00, 0x45, 0xFF, 0xFF, 0xFF, 0xA6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xCE, 0xFF, + 0xFF, 0xED, 0x15, 0x00, 0x00, 0xA5, 0xFF, 0xFF, 0xFF, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B, + 0xFF, 0xFF, 0xFF, 0x61, 0x00, 0x00, 0x00, 0x16, 0xEE, 0xFF, 0xFF, 0xDE, 0x0A, 0x00, 0x00, 0x00, + 0x29, 0xF8, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0xFF, 0xFF, 0xFF, 0x89, 0x00, + 0x00, 0x02, 0xC6, 0xFF, 0xFF, 0xF8, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC3, 0xFF, 0xFF, + 0xFB, 0x2E, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, + 0xF9, 0xFF, 0xFF, 0xC8, 0x02, 0x46, 0xFF, 0xFF, 0xFF, 0xA3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x45, 0xFF, 0xFF, 0xFF, 0xA1, 0x00, 0xB2, 0xFF, 0xFF, 0xFD, 0x2F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0xCF, 0xFF, 0xFF, 0xF3, 0x19, 0x00, 0x24, 0xF9, 0xFF, 0xFF, 0xB5, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xFF, 0x78, 0x00, 0x00, 0x00, 0x89, 0xFF, 0xFF, 0xFF, 0x3E, 0x00, + 0x00, 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xDD, 0x07, 0x00, 0x00, 0x00, 0x0D, 0xE7, 0xFF, 0xFF, 0xC7, + 0x00, 0x00, 0x00, 0x72, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFF, 0xFF, + 0xFF, 0x50, 0x00, 0x0F, 0xED, 0xFF, 0xFF, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xCA, + 0xFF, 0xFF, 0xD6, 0x03, 0x88, 0xFF, 0xFF, 0xFB, 0x2B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x38, 0xFE, 0xFF, 0xFF, 0x7E, 0xF6, 0xFF, 0xFF, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xA3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEC, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x1A, 0xF4, 0xFF, 0xFF, 0xFF, 0xFF, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A, 0xFF, 0xFF, 0xFF, 0xD2, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x8C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x8C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x8C, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x8C, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x8C, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x8C, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, + 0x8C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9C, 0x00, 0x00, 0x3F, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF9, 0xFF, 0xFF, 0xFF, 0x8C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x17, 0xE5, 0xFF, 0xFF, 0xDB, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xC0, 0xFF, 0xFF, 0xF5, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, + 0xFF, 0xFF, 0xFF, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0xFF, + 0xFF, 0xFF, 0x96, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0xF2, 0xFF, + 0xFF, 0xC9, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xD5, 0xFF, 0xFF, + 0xEB, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA9, 0xFF, 0xFF, 0xFD, + 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0xFF, 0xFF, 0xFF, 0x7A, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3D, 0xFB, 0xFF, 0xFF, 0xB2, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0xE7, 0xFF, 0xFF, 0xDD, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xC3, 0xFF, 0xFF, 0xF6, 0x2F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x5F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, + 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0x21, 0x38, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x24, 0x38, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x24, 0x00, 0x00, 0x00, 0x25, 0x93, 0xD4, 0xF4, 0xFA, 0xE6, + 0xAF, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x19, 0xF5, 0xFF, 0xFF, 0xE1, 0x6A, 0x63, 0xD3, 0xFF, 0xFF, + 0xFE, 0x32, 0x00, 0x00, 0x00, 0x75, 0xFF, 0xFF, 0xFF, 0x4D, 0x00, 0x00, 0x25, 0xFF, 0xFF, 0xFF, + 0x8A, 0x00, 0x00, 0x00, 0x0E, 0x22, 0x2D, 0x39, 0x08, 0x00, 0x00, 0x00, 0xED, 0xFF, 0xFF, 0xAC, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x0C, 0xDE, 0xFF, 0xFF, 0xB4, 0x00, + 0x00, 0x00, 0x00, 0x26, 0x95, 0xD5, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB4, 0x00, 0x00, + 0x00, 0x49, 0xF8, 0xFF, 0xFF, 0xFF, 0xFD, 0xF0, 0xEB, 0xFC, 0xFF, 0xFF, 0xB4, 0x00, 0x00, 0x03, + 0xE5, 0xFF, 0xFF, 0xE9, 0x46, 0x04, 0x00, 0x00, 0xDF, 0xFF, 0xFF, 0xB4, 0x00, 0x00, 0x2C, 0xFF, + 0xFF, 0xFF, 0x88, 0x00, 0x00, 0x00, 0x12, 0xFA, 0xFF, 0xFF, 0xB4, 0x00, 0x00, 0x34, 0xFF, 0xFF, + 0xFF, 0xA0, 0x00, 0x00, 0x03, 0xA2, 0xFF, 0xFF, 0xFF, 0xC4, 0x00, 0x00, 0x0D, 0xF8, 0xFF, 0xFF, + 0xFC, 0xA0, 0x88, 0xD1, 0xFF, 0xB6, 0xFF, 0xFF, 0xF9, 0x63, 0x34, 0x00, 0x84, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x91, 0x1D, 0xF8, 0xFF, 0xFF, 0xFF, 0xA0, 0x00, 0x01, 0x66, 0xCE, 0xF7, 0xF1, + 0xC4, 0x58, 0x00, 0x00, 0x50, 0xC9, 0xE5, 0xD2, 0x68, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, + 0xFF, 0xFF, 0xFF, 0x62, 0x0A, 0x80, 0xD6, 0xF6, 0xED, 0xB0, 0x30, 0x00, 0x00, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0xCB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x40, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0xD6, + 0xF7, 0x9A, 0x80, 0xC5, 0xFF, 0xFF, 0xFF, 0xDF, 0x06, 0x30, 0xFF, 0xFF, 0xFF, 0xFD, 0x3C, 0x00, + 0x00, 0x01, 0xAE, 0xFF, 0xFF, 0xFF, 0x50, 0x30, 0xFF, 0xFF, 0xFF, 0xB3, 0x00, 0x00, 0x00, 0x00, + 0x36, 0xFF, 0xFF, 0xFF, 0x92, 0x30, 0xFF, 0xFF, 0xFF, 0x74, 0x00, 0x00, 0x00, 0x00, 0x05, 0xFC, + 0xFF, 0xFF, 0xB9, 0x30, 0xFF, 0xFF, 0xFF, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xFF, 0xFF, + 0xC9, 0x30, 0xFF, 0xFF, 0xFF, 0x5E, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEE, 0xFF, 0xFF, 0xC8, 0x30, + 0xFF, 0xFF, 0xFF, 0x77, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFD, 0xFF, 0xFF, 0xB6, 0x30, 0xFF, 0xFF, + 0xFF, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x3C, 0xFF, 0xFF, 0xFF, 0x8C, 0x30, 0xFF, 0xFF, 0xFF, 0xFD, + 0x34, 0x00, 0x00, 0x01, 0xB4, 0xFF, 0xFF, 0xFF, 0x45, 0x32, 0xFF, 0xFF, 0xFF, 0xEA, 0xF2, 0x90, + 0x78, 0xC3, 0xFF, 0xFF, 0xFF, 0xD2, 0x02, 0x36, 0xFF, 0xFF, 0xFF, 0x64, 0xDC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xF0, 0x30, 0x00, 0x41, 0xFF, 0xFF, 0xFF, 0x3F, 0x13, 0x90, 0xDF, 0xF9, 0xE7, 0xA1, + 0x21, 0x00, 0x00, 0x00, 0x00, 0x03, 0x5F, 0xB7, 0xE9, 0xFA, 0xEE, 0xBE, 0x60, 0x02, 0x00, 0x00, + 0x00, 0x13, 0xC4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBE, 0x0D, 0x00, 0x01, 0xBA, 0xFF, + 0xFF, 0xFF, 0xAF, 0x7D, 0xB2, 0xFF, 0xFF, 0xFF, 0xAB, 0x00, 0x4B, 0xFF, 0xFF, 0xFF, 0x8A, 0x00, + 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x32, 0xA6, 0xFF, 0xFF, 0xFD, 0x15, 0x00, 0x00, 0x00, 0x21, + 0xF9, 0xEC, 0xDF, 0x67, 0xDC, 0xFF, 0xFF, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF2, 0xFF, 0xFF, 0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF4, 0xFF, + 0xFF, 0xC9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xE0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAC, 0xFF, 0xFF, 0xFE, 0x19, 0x00, 0x00, 0x00, + 0x09, 0xF3, 0xEE, 0xE2, 0x80, 0x54, 0xFF, 0xFF, 0xFF, 0x91, 0x00, 0x00, 0x00, 0x72, 0xFF, 0xFF, + 0xFF, 0x51, 0x02, 0xC4, 0xFF, 0xFF, 0xFF, 0xB0, 0x78, 0xA6, 0xFE, 0xFF, 0xFF, 0xC4, 0x01, 0x00, + 0x17, 0xCC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCA, 0x14, 0x00, 0x00, 0x00, 0x05, 0x65, + 0xBD, 0xEB, 0xFB, 0xE9, 0xBA, 0x62, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, + 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x00, + 0x00, 0x2E, 0xAB, 0xE9, 0xF8, 0xDB, 0x87, 0x0C, 0x7E, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x45, 0xF9, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD0, 0x84, 0xFF, 0xFF, 0xFF, 0x10, 0x0D, 0xE7, 0xFF, 0xFF, 0xFF, + 0xAD, 0x73, 0x96, 0xF9, 0xED, 0xFF, 0xFF, 0xFF, 0x10, 0x67, 0xFF, 0xFF, 0xFF, 0x8C, 0x00, 0x00, + 0x00, 0x50, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0xAE, 0xFF, 0xFF, 0xFE, 0x18, 0x00, 0x00, 0x00, 0x00, + 0xD7, 0xFF, 0xFF, 0xFF, 0x10, 0xD7, 0xFF, 0xFF, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0xFF, + 0xFF, 0xFF, 0x10, 0xE9, 0xFF, 0xFF, 0xCD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, + 0x10, 0xE9, 0xFF, 0xFF, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B, 0xFF, 0xFF, 0xFF, 0x10, 0xD9, + 0xFF, 0xFF, 0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0xFF, 0xFF, 0xFF, 0x10, 0xB0, 0xFF, 0xFF, + 0xFF, 0x1A, 0x00, 0x00, 0x00, 0x00, 0xD7, 0xFF, 0xFF, 0xFF, 0x10, 0x6C, 0xFF, 0xFF, 0xFF, 0x92, + 0x00, 0x00, 0x00, 0x5E, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0x11, 0xED, 0xFF, 0xFF, 0xFF, 0xB1, 0x76, + 0x9E, 0xFC, 0xE5, 0xFF, 0xFF, 0xFF, 0x12, 0x00, 0x53, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC6, + 0x74, 0xFF, 0xFF, 0xFF, 0x19, 0x00, 0x00, 0x3D, 0xBA, 0xF1, 0xF4, 0xD4, 0x7C, 0x08, 0x58, 0xFF, + 0xFF, 0xFF, 0x25, 0x00, 0x00, 0x07, 0x6A, 0xBE, 0xEC, 0xFA, 0xE9, 0xB5, 0x52, 0x00, 0x00, 0x00, + 0x00, 0x1C, 0xD2, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA2, 0x03, 0x00, 0x04, 0xCB, 0xFF, + 0xFF, 0xE8, 0x6E, 0x4A, 0x7B, 0xF2, 0xFF, 0xFF, 0x76, 0x00, 0x59, 0xFF, 0xFF, 0xFE, 0x32, 0x00, + 0x00, 0x00, 0x55, 0xFF, 0xFF, 0xED, 0x08, 0xAF, 0xFF, 0xFF, 0xDD, 0x00, 0x00, 0x00, 0x00, 0x04, + 0xF6, 0xFF, 0xFF, 0x46, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x76, 0xF4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x89, 0xF4, 0xFF, + 0xFF, 0xD5, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x22, 0xDF, 0xFF, 0xFF, 0xD9, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAD, 0xFF, 0xFF, 0xFC, 0x12, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xFF, 0xFF, 0xFF, 0x88, 0x00, 0x00, 0x00, 0x42, 0xF4, 0xDD, + 0xC6, 0x21, 0x05, 0xCF, 0xFF, 0xFF, 0xFF, 0xA8, 0x6B, 0x89, 0xF0, 0xFF, 0xFF, 0xB1, 0x00, 0x00, + 0x20, 0xD8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0x0F, 0x00, 0x00, 0x00, 0x0A, 0x70, + 0xC3, 0xEE, 0xF9, 0xE2, 0xBA, 0x5A, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xB0, 0xEB, 0xFA, + 0xE7, 0x95, 0x00, 0x00, 0x11, 0xE6, 0xFF, 0xFF, 0xFF, 0xFF, 0xB4, 0x00, 0x00, 0x60, 0xFF, 0xFF, + 0xFF, 0xB7, 0x6B, 0x53, 0x00, 0x00, 0x85, 0xFF, 0xFF, 0xFF, 0x11, 0x00, 0x00, 0x00, 0x00, 0x8C, + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB4, 0x90, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB4, 0x3A, 0x68, 0xBB, 0xFF, 0xFF, 0xFF, 0x68, 0x68, + 0x49, 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x8C, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0xFF, 0xFF, + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xAC, 0xE9, 0xF8, 0xDC, 0x8B, 0x0E, 0x4C, 0xFF, 0xFF, + 0xFF, 0x1E, 0x00, 0x48, 0xFA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD2, 0x6A, 0xFF, 0xFF, 0xFF, 0x15, + 0x0F, 0xE9, 0xFF, 0xFF, 0xFF, 0xB9, 0x7C, 0x9F, 0xFC, 0xE5, 0xFF, 0xFF, 0xFF, 0x11, 0x69, 0xFF, + 0xFF, 0xFF, 0x99, 0x00, 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0xAE, 0xFF, 0xFF, 0xFF, + 0x1D, 0x00, 0x00, 0x00, 0x01, 0xDD, 0xFF, 0xFF, 0xFF, 0x10, 0xD8, 0xFF, 0xFF, 0xE3, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x99, 0xFF, 0xFF, 0xFF, 0x10, 0xE9, 0xFF, 0xFF, 0xCD, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0x10, 0xEA, 0xFF, 0xFF, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B, + 0xFF, 0xFF, 0xFF, 0x10, 0xDA, 0xFF, 0xFF, 0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0xFF, 0xFF, + 0xFF, 0x10, 0xB3, 0xFF, 0xFF, 0xFF, 0x1B, 0x00, 0x00, 0x00, 0x00, 0xD6, 0xFF, 0xFF, 0xFF, 0x10, + 0x70, 0xFF, 0xFF, 0xFF, 0x97, 0x00, 0x00, 0x00, 0x5E, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0x15, 0xF1, + 0xFF, 0xFF, 0xFF, 0xBA, 0x7F, 0xA4, 0xFC, 0xE8, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x59, 0xFE, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x7E, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x40, 0xBB, 0xF1, 0xF5, + 0xD5, 0x7E, 0x08, 0x7E, 0xFF, 0xFF, 0xFF, 0x08, 0x00, 0x00, 0x00, 0x0A, 0x06, 0x00, 0x00, 0x00, + 0x00, 0x9C, 0xFF, 0xFF, 0xEF, 0x00, 0x2F, 0xCD, 0xEC, 0xFF, 0x7D, 0x00, 0x00, 0x00, 0x12, 0xEE, + 0xFF, 0xFF, 0xAE, 0x00, 0x07, 0xE1, 0xFF, 0xFF, 0xFC, 0x97, 0x61, 0x7A, 0xDF, 0xFF, 0xFF, 0xFD, + 0x38, 0x00, 0x00, 0x35, 0xEB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x62, 0x00, 0x00, + 0x00, 0x00, 0x12, 0x71, 0xC5, 0xE4, 0xFA, 0xF2, 0xD0, 0x8F, 0x26, 0x00, 0x00, 0x00, 0x30, 0xFF, + 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x60, 0x02, 0x6E, 0xD1, 0xF5, 0xEE, 0xB3, 0x32, + 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x58, 0xA9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF6, 0x36, 0x00, + 0x30, 0xFF, 0xFF, 0xFF, 0xAD, 0xFF, 0xD0, 0xBB, 0xF2, 0xFF, 0xFF, 0xFF, 0xC4, 0x00, 0x30, 0xFF, + 0xFF, 0xFF, 0xFF, 0x74, 0x00, 0x00, 0x22, 0xEA, 0xFF, 0xFF, 0xFF, 0x13, 0x30, 0xFF, 0xFF, 0xFF, + 0xD5, 0x01, 0x00, 0x00, 0x00, 0x89, 0xFF, 0xFF, 0xFF, 0x37, 0x30, 0xFF, 0xFF, 0xFF, 0x87, 0x00, + 0x00, 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xFF, 0x43, 0x30, 0xFF, 0xFF, 0xFF, 0x6A, 0x00, 0x00, 0x00, + 0x00, 0x49, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, + 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, + 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, + 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, + 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0x1E, 0xA0, 0xA0, 0xA0, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, + 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, + 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, + 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, + 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x1B, 0xA0, 0xA0, 0xA0, 0x3E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, + 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, + 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, + 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, + 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, + 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, + 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x30, 0xFF, + 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x52, 0xFF, 0xFF, 0xFF, 0x59, 0x3F, 0x9D, 0xE9, 0xFF, 0xFF, 0xFF, + 0x34, 0x68, 0xFF, 0xFF, 0xFF, 0xFF, 0xC6, 0x01, 0x5E, 0xF6, 0xFD, 0xE3, 0x9F, 0x15, 0x00, 0x30, + 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x2F, 0xF4, 0xFF, + 0xFF, 0xAC, 0x02, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x18, 0xE2, 0xFF, 0xFF, 0xC3, 0x08, + 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x09, 0xCA, 0xFF, 0xFF, 0xD7, 0x12, 0x00, 0x00, 0x30, + 0xFF, 0xFF, 0xFF, 0x64, 0x01, 0xAA, 0xFF, 0xFF, 0xE6, 0x1F, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0x84, 0xFF, 0xFF, 0xF2, 0x31, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0xBB, + 0xFF, 0xFF, 0xFF, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xE2, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0xE5, 0xFC, 0xFF, 0xFF, + 0x89, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0xB7, 0x11, 0x97, 0xFF, 0xFF, 0xFA, 0x2A, + 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x14, 0xEE, 0xFF, 0xFF, 0xBF, 0x00, 0x00, + 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x6F, 0xFF, 0xFF, 0xFF, 0x5B, 0x00, 0x00, 0x30, + 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x04, 0xD6, 0xFF, 0xFF, 0xE7, 0x0F, 0x00, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0x00, 0x00, 0x00, 0x46, 0xFF, 0xFF, 0xFF, 0x92, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, + 0x00, 0x00, 0x00, 0x00, 0xB2, 0xFF, 0xFF, 0xFC, 0x31, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, + 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, + 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, + 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, + 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x30, 0xFF, + 0xFF, 0xFF, 0x64, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x42, 0xFF, 0xFF, 0xFF, 0x27, 0x0B, 0x89, 0xE1, + 0xF6, 0xD5, 0x6D, 0x00, 0x00, 0x12, 0x93, 0xE2, 0xF8, 0xD9, 0x6F, 0x00, 0x00, 0x36, 0xFF, 0xFF, + 0xFF, 0x3B, 0xBE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7A, 0x0A, 0xCE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x7D, 0x00, 0x31, 0xFF, 0xFF, 0xFF, 0xA9, 0xFC, 0xBC, 0xDA, 0xFF, 0xFF, 0xFF, 0xEF, 0x85, 0xFD, + 0xBE, 0xD6, 0xFF, 0xFF, 0xFF, 0xF8, 0x11, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0x4D, 0x00, 0x03, 0xBA, + 0xFF, 0xFF, 0xFF, 0xFF, 0x5A, 0x00, 0x01, 0xAD, 0xFF, 0xFF, 0xFF, 0x52, 0x30, 0xFF, 0xFF, 0xFF, + 0xC6, 0x00, 0x00, 0x00, 0x4F, 0xFF, 0xFF, 0xFF, 0xD1, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, + 0x74, 0x30, 0xFF, 0xFF, 0xFF, 0x82, 0x00, 0x00, 0x00, 0x29, 0xFF, 0xFF, 0xFF, 0x91, 0x00, 0x00, + 0x00, 0x19, 0xFF, 0xFF, 0xFF, 0x7F, 0x30, 0xFF, 0xFF, 0xFF, 0x69, 0x00, 0x00, 0x00, 0x19, 0xFF, + 0xFF, 0xFF, 0x73, 0x00, 0x00, 0x00, 0x09, 0xFF, 0xFF, 0xFF, 0x80, 0x30, 0xFF, 0xFF, 0xFF, 0x64, + 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xFF, 0xFF, 0x80, + 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0x00, + 0x08, 0xFF, 0xFF, 0xFF, 0x80, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, + 0xFF, 0x70, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xFF, 0xFF, 0x80, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, + 0x00, 0x00, 0x18, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xFF, 0xFF, 0x80, 0x30, + 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0x00, 0x08, + 0xFF, 0xFF, 0xFF, 0x80, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x18, 0xFF, 0xFF, 0xFF, + 0x70, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xFF, 0xFF, 0x80, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, + 0x00, 0x18, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0x00, 0x08, 0xFF, 0xFF, 0xFF, 0x80, 0x42, 0xFF, + 0xFF, 0xFF, 0x27, 0x03, 0x6F, 0xD2, 0xF5, 0xEE, 0xB3, 0x32, 0x00, 0x00, 0x36, 0xFF, 0xFF, 0xFF, + 0x39, 0xAA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF6, 0x36, 0x00, 0x31, 0xFF, 0xFF, 0xFF, 0xA3, 0xFF, + 0xD0, 0xBB, 0xF2, 0xFF, 0xFF, 0xFF, 0xC4, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0x74, 0x00, 0x00, + 0x22, 0xEA, 0xFF, 0xFF, 0xFF, 0x13, 0x30, 0xFF, 0xFF, 0xFF, 0xD5, 0x01, 0x00, 0x00, 0x00, 0x89, + 0xFF, 0xFF, 0xFF, 0x37, 0x30, 0xFF, 0xFF, 0xFF, 0x87, 0x00, 0x00, 0x00, 0x00, 0x5C, 0xFF, 0xFF, + 0xFF, 0x43, 0x30, 0xFF, 0xFF, 0xFF, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x49, 0xFF, 0xFF, 0xFF, 0x44, + 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, + 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, + 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x48, 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, + 0xFF, 0xFF, 0xFF, 0x44, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x48, 0xFF, 0xFF, + 0xFF, 0x44, 0x00, 0x00, 0x00, 0x44, 0xA6, 0xDF, 0xF6, 0xF5, 0xDE, 0xA5, 0x43, 0x00, 0x00, 0x00, + 0x00, 0x09, 0xA7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA6, 0x07, 0x00, 0x00, 0xA5, + 0xFF, 0xFF, 0xFF, 0xD0, 0x82, 0x82, 0xD1, 0xFF, 0xFF, 0xFF, 0x9B, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, + 0xAE, 0x02, 0x00, 0x00, 0x03, 0xB7, 0xFF, 0xFF, 0xFF, 0x2E, 0xA2, 0xFF, 0xFF, 0xFF, 0x24, 0x00, + 0x00, 0x00, 0x00, 0x33, 0xFF, 0xFF, 0xFF, 0x87, 0xDB, 0xFF, 0xFF, 0xE3, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0xF6, 0xFF, 0xFF, 0xBD, 0xF3, 0xFF, 0xFF, 0xC9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xE1, 0xFF, 0xFF, 0xD2, 0xF2, 0xFF, 0xFF, 0xC9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE3, 0xFF, + 0xFF, 0xD1, 0xDA, 0xFF, 0xFF, 0xE3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF8, 0xFF, 0xFF, 0xB9, + 0xA0, 0xFF, 0xFF, 0xFF, 0x24, 0x00, 0x00, 0x00, 0x00, 0x3D, 0xFF, 0xFF, 0xFF, 0x7F, 0x3F, 0xFF, + 0xFF, 0xFF, 0xAD, 0x01, 0x00, 0x00, 0x07, 0xC3, 0xFF, 0xFF, 0xFB, 0x20, 0x00, 0xA5, 0xFF, 0xFF, + 0xFF, 0xC7, 0x7A, 0x83, 0xD8, 0xFF, 0xFF, 0xFF, 0x81, 0x00, 0x00, 0x09, 0xA8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8C, 0x01, 0x00, 0x00, 0x00, 0x00, 0x47, 0xA8, 0xE1, 0xF8, 0xF4, + 0xDA, 0x9B, 0x32, 0x00, 0x00, 0x00, 0x42, 0xFF, 0xFF, 0xFF, 0x3B, 0x0E, 0x87, 0xD9, 0xF6, 0xEC, + 0xAE, 0x2C, 0x00, 0x00, 0x36, 0xFF, 0xFF, 0xFF, 0x5A, 0xD6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF6, + 0x3A, 0x00, 0x32, 0xFF, 0xFF, 0xFF, 0xE0, 0xF7, 0x9B, 0x80, 0xC9, 0xFF, 0xFF, 0xFF, 0xDA, 0x05, + 0x30, 0xFF, 0xFF, 0xFF, 0xFF, 0x41, 0x00, 0x00, 0x02, 0xB5, 0xFF, 0xFF, 0xFF, 0x4D, 0x30, 0xFF, + 0xFF, 0xFF, 0xC2, 0x00, 0x00, 0x00, 0x00, 0x3B, 0xFF, 0xFF, 0xFF, 0x91, 0x30, 0xFF, 0xFF, 0xFF, + 0x7B, 0x00, 0x00, 0x00, 0x00, 0x06, 0xFC, 0xFF, 0xFF, 0xB8, 0x30, 0xFF, 0xFF, 0xFF, 0x61, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xED, 0xFF, 0xFF, 0xC9, 0x30, 0xFF, 0xFF, 0xFF, 0x5E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xEE, 0xFF, 0xFF, 0xC8, 0x30, 0xFF, 0xFF, 0xFF, 0x78, 0x00, 0x00, 0x00, 0x00, 0x07, + 0xFD, 0xFF, 0xFF, 0xB6, 0x30, 0xFF, 0xFF, 0xFF, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x3D, 0xFF, 0xFF, + 0xFF, 0x8C, 0x30, 0xFF, 0xFF, 0xFF, 0xFE, 0x35, 0x00, 0x00, 0x01, 0xB6, 0xFF, 0xFF, 0xFF, 0x45, + 0x30, 0xFF, 0xFF, 0xFF, 0xE9, 0xF3, 0x8F, 0x78, 0xC5, 0xFF, 0xFF, 0xFF, 0xD2, 0x02, 0x30, 0xFF, + 0xFF, 0xFF, 0x72, 0xDD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x2F, 0x00, 0x30, 0xFF, 0xFF, 0xFF, + 0x63, 0x13, 0x90, 0xDF, 0xF9, 0xE6, 0xA0, 0x20, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x2A, 0xA8, 0xE8, 0xF8, 0xDC, 0x8B, 0x0E, 0x59, 0xFF, 0xFF, 0xFF, 0x1E, 0x00, 0x40, + 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD2, 0x77, 0xFF, 0xFF, 0xFF, 0x15, 0x0B, 0xE4, 0xFF, 0xFF, + 0xFF, 0xB9, 0x7C, 0x9D, 0xFA, 0xEC, 0xFF, 0xFF, 0xFF, 0x11, 0x64, 0xFF, 0xFF, 0xFF, 0x99, 0x00, + 0x00, 0x00, 0x54, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0xAC, 0xFF, 0xFF, 0xFF, 0x1C, 0x00, 0x00, 0x00, + 0x00, 0xD9, 0xFF, 0xFF, 0xFF, 0x10, 0xD7, 0xFF, 0xFF, 0xE3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, + 0xFF, 0xFF, 0xFF, 0x10, 0xE9, 0xFF, 0xFF, 0xCD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, + 0xFF, 0x10, 0xE9, 0xFF, 0xFF, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0xFF, 0xFF, 0xFF, 0x10, + 0xD9, 0xFF, 0xFF, 0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9B, 0xFF, 0xFF, 0xFF, 0x10, 0xB1, 0xFF, + 0xFF, 0xFF, 0x1A, 0x00, 0x00, 0x00, 0x01, 0xDF, 0xFF, 0xFF, 0xFF, 0x10, 0x6D, 0xFF, 0xFF, 0xFF, + 0x93, 0x00, 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0x12, 0xEE, 0xFF, 0xFF, 0xFF, 0xB2, + 0x77, 0x9D, 0xFB, 0xE5, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x55, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xCD, 0x7A, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x3F, 0xBB, 0xF1, 0xF4, 0xD4, 0x7F, 0x0A, 0x77, + 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, + 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xFF, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xFF, 0x10, 0x43, 0xFF, 0xFF, 0xFF, 0x27, 0x30, + 0xCA, 0xF9, 0xA7, 0x37, 0xFF, 0xFF, 0xFF, 0x40, 0xE2, 0xFF, 0xFF, 0xB8, 0x31, 0xFF, 0xFF, 0xFF, + 0xA6, 0xFF, 0xFF, 0xFF, 0xB8, 0x30, 0xFF, 0xFF, 0xFF, 0xFB, 0xA9, 0x1A, 0x07, 0x17, 0x30, 0xFF, + 0xFF, 0xFF, 0xE1, 0x06, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x91, 0x00, 0x00, 0x00, 0x00, + 0x30, 0xFF, 0xFF, 0xFF, 0x6D, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, + 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, + 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, + 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x30, + 0xFF, 0xFF, 0xFF, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x9F, 0xD9, 0xF5, 0xF7, + 0xDD, 0xB7, 0x56, 0x04, 0x00, 0x00, 0x00, 0x00, 0x7B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xC9, 0x10, 0x00, 0x00, 0x32, 0xFE, 0xFF, 0xFF, 0xAF, 0x6F, 0x66, 0x8A, 0xEF, 0xFF, 0xFF, + 0xA2, 0x00, 0x00, 0x7B, 0xFF, 0xFF, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x4A, 0xF1, 0xD6, 0xB4, 0x02, + 0x00, 0x7E, 0xFF, 0xFF, 0xF2, 0x3D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, + 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xA5, 0x6D, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xDD, 0x67, 0x01, 0x00, 0x00, 0x00, 0x00, 0x4D, 0xB6, 0xF7, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x39, 0x6E, + 0xAE, 0xF9, 0xFF, 0xFF, 0xFF, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, + 0xFB, 0xFF, 0xFF, 0x5A, 0x01, 0x98, 0xC6, 0xEB, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x08, 0xF4, 0xFF, + 0xFF, 0x4D, 0x00, 0x95, 0xFF, 0xFF, 0xF3, 0x91, 0x68, 0x61, 0x78, 0xCD, 0xFF, 0xFF, 0xEB, 0x0C, + 0x00, 0x0F, 0xBE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x3F, 0x00, 0x00, 0x00, + 0x06, 0x66, 0xB6, 0xE3, 0xF9, 0xFC, 0xEE, 0xC6, 0x7F, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, + 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x94, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0xB0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x30, + 0xB0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x30, 0x47, 0x68, 0xF6, 0xFF, 0xFF, 0xC2, 0x68, + 0x68, 0x13, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, + 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, + 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF0, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x9A, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xEC, 0xFF, 0xFF, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCE, 0xFF, 0xFF, 0xFF, + 0xA2, 0x81, 0x35, 0x00, 0x00, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x58, 0x00, 0x00, 0x05, 0x82, + 0xDF, 0xFA, 0xEB, 0xC3, 0x35, 0x64, 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, + 0xFF, 0xFF, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, + 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x64, + 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x64, 0xFF, 0xFF, + 0xFF, 0x30, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x30, + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, + 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x31, 0x00, 0x00, 0x00, 0x00, + 0x86, 0xFF, 0xFF, 0xFF, 0x10, 0x63, 0xFF, 0xFF, 0xFF, 0x43, 0x00, 0x00, 0x00, 0x00, 0xA4, 0xFF, + 0xFF, 0xFF, 0x10, 0x59, 0xFF, 0xFF, 0xFF, 0x6F, 0x00, 0x00, 0x00, 0x0A, 0xEB, 0xFF, 0xFF, 0xFF, + 0x10, 0x35, 0xFF, 0xFF, 0xFF, 0xDC, 0x16, 0x00, 0x01, 0x91, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0x03, + 0xE3, 0xFF, 0xFF, 0xFF, 0xED, 0xB8, 0xD8, 0xFF, 0xA3, 0xFF, 0xFF, 0xFF, 0x12, 0x00, 0x53, 0xFD, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8E, 0x56, 0xFF, 0xFF, 0xFF, 0x17, 0x00, 0x00, 0x45, 0xBF, 0xF3, + 0xF4, 0xCB, 0x62, 0x00, 0x45, 0xFF, 0xFF, 0xFF, 0x24, 0xB8, 0xFF, 0xFF, 0xF9, 0x0D, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x8E, 0xFF, 0xFF, 0xFF, 0x29, 0x5E, 0xFF, 0xFF, 0xFF, 0x54, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xDF, 0xFF, 0xFF, 0xCC, 0x00, 0x0D, 0xF6, 0xFF, 0xFF, 0xA2, 0x00, 0x00, 0x00, 0x00, + 0x31, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xEC, 0x03, 0x00, 0x00, 0x00, 0x82, + 0xFF, 0xFF, 0xFC, 0x17, 0x00, 0x00, 0x51, 0xFF, 0xFF, 0xFF, 0x3D, 0x00, 0x00, 0x00, 0xD4, 0xFF, + 0xFF, 0xB7, 0x00, 0x00, 0x00, 0x07, 0xEF, 0xFF, 0xFF, 0x8B, 0x00, 0x00, 0x25, 0xFF, 0xFF, 0xFF, + 0x5A, 0x00, 0x00, 0x00, 0x00, 0x9D, 0xFF, 0xFF, 0xD9, 0x00, 0x00, 0x76, 0xFF, 0xFF, 0xF3, 0x0A, + 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x26, 0x00, 0xC6, 0xFF, 0xFF, 0xA1, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0xE6, 0xFF, 0xFF, 0x72, 0x18, 0xFE, 0xFF, 0xFF, 0x44, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xBA, 0x62, 0xFF, 0xFF, 0xE5, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x36, 0xFF, 0xFF, 0xF7, 0xB2, 0xFF, 0xFF, 0x8B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xDC, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xD2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x29, 0xFF, 0xFF, 0xFF, 0xFF, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xF0, 0xFF, 0xFF, 0x62, + 0x00, 0x00, 0x00, 0x02, 0xEF, 0xFF, 0xFF, 0xFF, 0x24, 0x00, 0x00, 0x00, 0x24, 0xFF, 0xFF, 0xFF, + 0x26, 0x00, 0xB2, 0xFF, 0xFF, 0x98, 0x00, 0x00, 0x00, 0x34, 0xFF, 0xFF, 0xFF, 0xFF, 0x65, 0x00, + 0x00, 0x00, 0x5C, 0xFF, 0xFF, 0xE4, 0x00, 0x00, 0x71, 0xFF, 0xFF, 0xCD, 0x00, 0x00, 0x00, 0x77, + 0xFF, 0xFF, 0xF4, 0xFF, 0xA6, 0x00, 0x00, 0x00, 0x94, 0xFF, 0xFF, 0xA2, 0x00, 0x00, 0x30, 0xFF, + 0xFF, 0xFA, 0x08, 0x00, 0x00, 0xB9, 0xFF, 0xE7, 0xB0, 0xFF, 0xE7, 0x00, 0x00, 0x00, 0xCB, 0xFF, + 0xFF, 0x60, 0x00, 0x00, 0x01, 0xED, 0xFF, 0xFF, 0x38, 0x00, 0x06, 0xF5, 0xFF, 0xAE, 0x7C, 0xFF, + 0xFF, 0x29, 0x00, 0x08, 0xFA, 0xFF, 0xFF, 0x1E, 0x00, 0x00, 0x00, 0xAE, 0xFF, 0xFF, 0x6E, 0x00, + 0x3F, 0xFF, 0xFF, 0x72, 0x42, 0xFF, 0xFF, 0x6A, 0x00, 0x3B, 0xFF, 0xFF, 0xDC, 0x00, 0x00, 0x00, + 0x00, 0x6D, 0xFF, 0xFF, 0xA3, 0x00, 0x81, 0xFF, 0xFF, 0x31, 0x09, 0xF8, 0xFF, 0xAB, 0x00, 0x72, + 0xFF, 0xFF, 0x9A, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xFF, 0xFF, 0xD9, 0x00, 0xC4, 0xFF, 0xED, 0x02, + 0x00, 0xC0, 0xFF, 0xEB, 0x01, 0xAA, 0xFF, 0xFF, 0x58, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE9, 0xFF, + 0xFE, 0x19, 0xFA, 0xFF, 0xAC, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0x2C, 0xE2, 0xFF, 0xFF, 0x17, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xA9, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0x6A, 0x00, 0x00, 0x3E, 0xFF, 0xFF, + 0x7C, 0xFF, 0xFF, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, + 0x28, 0x00, 0x00, 0x06, 0xF6, 0xFF, 0xE1, 0xFF, 0xFF, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x27, 0xFF, 0xFF, 0xFF, 0xFF, 0xE5, 0x00, 0x00, 0x00, 0x00, 0xBB, 0xFF, 0xFF, 0xFF, 0xFF, 0x50, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE5, 0xFF, 0xFF, 0xFF, 0xA3, 0x00, 0x00, 0x00, 0x00, + 0x7A, 0xFF, 0xFF, 0xFF, 0xFD, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xFF, 0xFF, + 0xFF, 0x61, 0x00, 0x00, 0x00, 0x00, 0x39, 0xFF, 0xFF, 0xFF, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x3D, + 0xFD, 0xFF, 0xFF, 0x95, 0x00, 0x00, 0x00, 0x00, 0x34, 0xFC, 0xFF, 0xFF, 0xA0, 0x00, 0x00, 0x85, + 0xFF, 0xFF, 0xFE, 0x3B, 0x00, 0x00, 0x04, 0xD0, 0xFF, 0xFF, 0xDD, 0x0D, 0x00, 0x00, 0x05, 0xCB, + 0xFF, 0xFF, 0xD7, 0x07, 0x00, 0x79, 0xFF, 0xFF, 0xFC, 0x39, 0x00, 0x00, 0x00, 0x00, 0x26, 0xF4, + 0xFF, 0xFF, 0x82, 0x24, 0xF7, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0xFF, + 0xFF, 0xFA, 0xCE, 0xFF, 0xFF, 0xC6, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB1, 0xFF, + 0xFF, 0xFF, 0xFF, 0xF2, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0xF1, 0xFF, + 0xFF, 0xFF, 0x6B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0xFF, 0xFF, 0xFF, + 0xFF, 0xBB, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xEA, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0xFF, 0xFF, 0xD1, 0x74, 0xFF, 0xFF, + 0xF7, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0xFF, 0xFF, 0xFD, 0x35, 0x03, 0xCC, 0xFF, 0xFF, + 0xCD, 0x05, 0x00, 0x00, 0x00, 0x23, 0xF3, 0xFF, 0xFF, 0x8F, 0x00, 0x00, 0x31, 0xFC, 0xFF, 0xFF, + 0x85, 0x00, 0x00, 0x03, 0xC5, 0xFF, 0xFF, 0xE0, 0x0B, 0x00, 0x00, 0x00, 0x8D, 0xFF, 0xFF, 0xFC, + 0x3A, 0x00, 0x7B, 0xFF, 0xFF, 0xFF, 0x49, 0x00, 0x00, 0x00, 0x00, 0x0B, 0xE0, 0xFF, 0xFF, 0xDD, + 0x0C, 0x9B, 0xFF, 0xFF, 0xFF, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8F, 0xFF, 0xFF, 0xFF, 0x25, + 0x37, 0xFF, 0xFF, 0xFF, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0xFF, 0xFF, 0xC7, 0x00, 0x00, + 0xD3, 0xFF, 0xFF, 0xC8, 0x00, 0x00, 0x00, 0x00, 0x29, 0xFF, 0xFF, 0xFF, 0x69, 0x00, 0x00, 0x70, + 0xFF, 0xFF, 0xFE, 0x1C, 0x00, 0x00, 0x00, 0x76, 0xFF, 0xFF, 0xF9, 0x12, 0x00, 0x00, 0x13, 0xF9, + 0xFF, 0xFF, 0x6E, 0x00, 0x00, 0x00, 0xC4, 0xFF, 0xFF, 0xAE, 0x00, 0x00, 0x00, 0x00, 0xA9, 0xFF, + 0xFF, 0xC2, 0x00, 0x00, 0x14, 0xFD, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x46, 0xFF, 0xFF, + 0xFD, 0x17, 0x00, 0x5D, 0xFF, 0xFF, 0xEC, 0x05, 0x00, 0x00, 0x00, 0x00, 0x02, 0xE0, 0xFF, 0xFF, + 0x67, 0x00, 0xA5, 0xFF, 0xFF, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xB5, + 0x00, 0xE8, 0xFF, 0xFF, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0xFD, 0xFF, 0xF5, 0x35, + 0xFF, 0xFF, 0xD9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB8, 0xFF, 0xFF, 0xB3, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0xEB, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8E, 0xFF, 0xFF, 0xFF, 0x63, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9D, 0xFF, 0xFF, 0xF6, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0xFD, 0xFF, 0xFF, 0x8F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x73, 0xA1, 0xB9, 0xFE, 0xFF, 0xFF, 0xEA, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xA8, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x92, 0xF4, 0xFB, 0xE2, 0xA7, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x70, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x70, 0x00, 0x40, 0x94, 0x94, 0x94, 0x94, 0x94, 0xDA, 0xFF, 0xFF, 0xFF, + 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0xF9, 0xFF, 0xFF, 0xBC, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0B, 0xD9, 0xFF, 0xFF, 0xEA, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0xFF, + 0xFF, 0xFF, 0x4B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xFF, 0xFF, 0xFF, 0x91, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x21, 0xF1, 0xFF, 0xFF, 0xCF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0xC7, 0xFF, 0xFF, 0xF5, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xFF, 0xFF, 0xFF, 0x63, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0xFD, 0xFF, 0xFF, 0xA9, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xDF, 0xFF, 0xFF, 0xFF, 0xAB, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x09, 0xF4, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0xF4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, + 0xDE, 0xFF, 0xFF, 0xA7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0E, 0xCF, 0xFF, 0xFF, 0x98, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x06, 0xBD, 0xFF, 0xF7, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0xFF, 0xE0, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xF5, + 0xFF, 0xFF, 0xFF, 0xC5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x5F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xBC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0xFD, 0xFF, 0xFF, 0xA7, 0xFF, 0xFF, 0xDA, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xFF, 0xFF, 0xE8, 0x28, 0xFF, 0xFF, + 0xFF, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD3, 0xFF, 0xFF, 0x99, + 0x00, 0xD8, 0xFF, 0xFF, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xFF, + 0xFF, 0xFF, 0x47, 0x00, 0x86, 0xFF, 0xFF, 0xEB, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x8D, 0xFF, 0xFF, 0xEE, 0x05, 0x00, 0x32, 0xFF, 0xFF, 0xFF, 0x4D, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xE6, 0xFF, 0xFF, 0xA0, 0x00, 0x00, 0x00, 0xDD, 0xFF, 0xFF, 0xA9, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0xFF, 0xFF, 0xFF, 0x4C, 0x00, 0x00, 0x00, 0x8A, 0xFF, + 0xFF, 0xF7, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA3, 0xFF, 0xFF, 0xF2, 0x07, 0x00, 0x00, + 0x00, 0x36, 0xFF, 0xFF, 0xFF, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0xF4, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1D, 0x00, 0x00, 0x00, + 0x00, 0xBA, 0xFF, 0xFF, 0xFD, 0xD0, 0xD0, 0xD0, 0xD0, 0xD0, 0xD0, 0xD8, 0xFF, 0xFF, 0xFF, 0x77, + 0x00, 0x00, 0x00, 0x1A, 0xFD, 0xFF, 0xFF, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xEB, + 0xFF, 0xFF, 0xD4, 0x00, 0x00, 0x00, 0x74, 0xFF, 0xFF, 0xFF, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x00, 0x00, 0xD1, 0xFF, 0xFF, 0xEE, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x2E, 0xFF, 0xFF, 0xFF, 0x8D, 0x00, 0x2D, 0xFF, 0xFF, 0xFF, 0x94, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCD, 0xFF, 0xFF, 0xE6, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xAC, 0xFF, 0xFF, 0xEA, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0xFF, 0xFF, 0xC9, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7C, 0xFF, 0xFF, 0x9A, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xF8, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD0, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xD0, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xB9, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0x07, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0xF1, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0xE4, 0x4A, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x54, 0x00, 0x00, + 0x13, 0xD7, 0xFF, 0xFF, 0xB3, 0x00, 0x0A, 0xC7, 0xFF, 0xFF, 0xA1, 0x09, 0x03, 0xB3, 0xFF, 0xF9, + 0x6A, 0x00, 0x00, 0x87, 0xFF, 0xE5, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, + 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, + 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, + 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, + 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, + 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, + 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x44, + 0xFF, 0xFF, 0xFF, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0xDC, 0xF5, 0xA5, 0x1F, 0x00, + 0x00, 0x80, 0xFF, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0xF6, 0xFF, 0xFF, 0xFF, 0xF5, + 0x77, 0x38, 0xDE, 0xFF, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0xFF, 0xB1, 0x35, 0x97, + 0xFE, 0xFF, 0xFF, 0xFF, 0xD2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0xFF, 0x46, 0x00, + 0x00, 0x36, 0xC0, 0xF6, 0xC4, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xFF, 0xFF, 0xFF, + 0xFF, 0x4B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, + 0xFF, 0xFF, 0xD7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0x6D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0x11, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, + 0x44, 0xFF, 0xFF, 0xEB, 0xE8, 0xFF, 0xFF, 0x8F, 0x00, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, 0xFF, + 0x08, 0x44, 0xFF, 0xFF, 0xF6, 0x65, 0xFF, 0xFF, 0xFA, 0x26, 0x00, 0x00, 0x00, 0x4C, 0xFF, 0xFF, + 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x09, 0xD2, 0xFF, 0xFF, 0xB1, 0x00, 0x00, 0x00, 0x4C, 0xFF, + 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x0F, 0x45, 0xFF, 0xFF, 0xFF, 0x42, 0x00, 0x00, 0x4C, + 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0xB6, 0xFF, 0xFF, 0xD0, 0x02, 0x00, + 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x2A, 0xFC, 0xFF, 0xFF, 0x64, + 0x00, 0x4C, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x96, 0xFF, 0xFF, + 0xE7, 0x0C, 0x4A, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x15, 0xF1, + 0xFF, 0xFF, 0x85, 0x44, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, 0x00, + 0x77, 0xFF, 0xFF, 0xF7, 0x55, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, 0x00, + 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xC6, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x57, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, 0xFF, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x44, 0xFF, 0xFF, + 0xFF, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA8, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xFD, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3D, 0xF7, 0xFF, 0xF2, 0x53, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0xEF, 0xFF, 0xD7, 0x29, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xE3, 0xFF, + 0xAD, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x13, 0x72, 0xB5, 0xE1, 0xF4, 0xF7, 0xE4, 0xBE, 0x7E, 0x1E, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x77, 0xF4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFA, 0x8F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA4, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFB, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0x07, 0x00, 0x00, 0x00, 0x75, 0xFF, 0xFF, 0xFF, 0xFA, 0x8D, + 0x2B, 0x03, 0x01, 0x21, 0x7A, 0xF3, 0xFF, 0xFF, 0xFF, 0x9B, 0x00, 0x00, 0x11, 0xF1, 0xFF, 0xFF, + 0xFD, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0xF4, 0xFF, 0xFF, 0xFE, 0x2E, 0x00, 0x6C, + 0xFF, 0xFF, 0xFF, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0xFF, 0xFF, 0xFF, + 0x98, 0x00, 0xAD, 0xFF, 0xFF, 0xFF, 0x2E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, + 0xFA, 0xFF, 0xFF, 0xDA, 0x00, 0xD5, 0xFF, 0xFF, 0xF3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xCF, 0xFF, 0xFF, 0xFE, 0x04, 0xE8, 0xFF, 0xFF, 0xDE, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB7, 0xFF, 0xFF, 0xFF, 0x17, 0xE5, 0xFF, 0xFF, 0xE1, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBA, 0xFF, 0xFF, 0xFF, 0x17, 0xD2, 0xFF, + 0xFF, 0xF7, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD2, 0xFF, 0xFF, 0xFC, + 0x06, 0xA5, 0xFF, 0xFF, 0xFF, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xFC, + 0xFF, 0xFF, 0xD4, 0x00, 0x61, 0xFF, 0xFF, 0xFF, 0xA6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x74, 0xFF, 0xFF, 0xFF, 0x89, 0x00, 0x0A, 0xEA, 0xFF, 0xFF, 0xFF, 0x5D, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x32, 0xF4, 0xFF, 0xFF, 0xFA, 0x21, 0x00, 0x00, 0x63, 0xFF, 0xFF, 0xFF, 0xFD, + 0x97, 0x2E, 0x03, 0x01, 0x20, 0x7A, 0xF3, 0xFF, 0xFF, 0xFF, 0x74, 0x00, 0x00, 0x00, 0x00, 0x91, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA8, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x6B, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x6F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x6F, 0xB5, 0xE2, 0xF5, 0xFA, 0xE7, 0xBD, 0x77, + 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xB6, + 0xFF, 0xFF, 0xE3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, + 0xFF, 0xFF, 0xC2, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, + 0xFF, 0xFF, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, + 0xFF, 0xF5, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xFF, 0xFF, 0xFF, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, + 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, + 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, + 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, + 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, 0xFF, + 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, 0xFF, + 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xFF, + 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, + 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x90, 0xFF, 0xFF, 0xFF, 0x30, 0x70, 0xFF, 0xFF, 0xFF, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x91, 0xFF, 0xFF, 0xFF, 0x2F, 0x68, 0xFF, 0xFF, 0xFF, 0x5C, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xA1, 0xFF, 0xFF, 0xFF, 0x22, 0x51, 0xFF, 0xFF, 0xFF, 0x8B, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xD8, 0xFF, 0xFF, 0xFC, 0x0A, 0x19, 0xFF, 0xFF, 0xFF, 0xE9, 0x12, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x57, 0xFF, 0xFF, 0xFF, 0xC5, 0x00, 0x00, 0xB6, 0xFF, 0xFF, 0xFF, 0xD0, + 0x42, 0x04, 0x00, 0x15, 0x75, 0xF8, 0xFF, 0xFF, 0xFF, 0x55, 0x00, 0x00, 0x28, 0xF0, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFC, 0xF5, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA6, 0x00, 0x00, 0x00, 0x00, 0x36, 0xDE, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x8D, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x60, 0xAF, 0xE1, 0xF6, 0xFA, 0xEA, 0xC5, 0x87, 0x24, 0x00, 0x00, 0x00, 0x00 +}; diff --git a/sketch/obd2_gauge/bluetooth.h b/sketch/obd2_gauge/bluetooth.h index 4e4f0b9..f784a7a 100644 --- a/sketch/obd2_gauge/bluetooth.h +++ b/sketch/obd2_gauge/bluetooth.h @@ -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(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); - } - lastActivityTime = millis(); - return 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; } + +}//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; } \ No newline at end of file diff --git a/sketch/obd2_gauge/config.h b/sketch/obd2_gauge/config.h index e0a9e26..1ef87c4 100644 --- a/sketch/obd2_gauge/config.h +++ b/sketch/obd2_gauge/config.h @@ -12,6 +12,25 @@ extern uint8_t autoSleepIndex; extern uint32_t lastActivityTime; extern uint8_t beepSoundType; extern bool wokeFromSleep; +extern uint8_t transitionType; +extern volatile bool systemSleeping; +extern TaskHandle_t btTaskHandle; + +// Control de Inactividad de 10 segundos en Menu de Configuracion y Submenus +static uint32_t lastMenuActivityTime = 0; +static bool menuExitedDueToTimeout = false; + +inline void resetMenuActivity() { + lastMenuActivityTime = millis(); +} + +inline bool checkMenuTimeout() { + if (millis() - lastMenuActivityTime >= 10000) { + menuExitedDueToTimeout = true; + return true; + } + return false; +} /*------------ SOUND EFFECTS ----------*/ const char* const BEEP_NAME_TABLE[6] = { @@ -180,9 +199,11 @@ void warningMenu() { waitForTouchRelease(); while (true) { + if (checkMenuTimeout()) break; autoDim(); uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); // 1. Direct touch on any parameter row to toggle ON/OFF if (ty >= 6 && ty < 184) { int8_t row = (ty - 10) / 44; @@ -205,10 +226,11 @@ void warningMenu() { break; } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); delay(200); break; } - yield(); + delay(25); } } @@ -342,9 +364,11 @@ void alarmSoundMenu() { waitForTouchRelease(); while (true) { + if (checkMenuTimeout()) break; autoDim(); uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); // 1. Touch on pattern selection (y: 0 to 122) if (ty < 122) { int8_t col = (tx < 160) ? 0 : 1; @@ -379,10 +403,11 @@ void alarmSoundMenu() { break; } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); delay(200); break; } - yield(); + delay(25); } } @@ -441,9 +466,11 @@ void beepSoundMenu() { waitForTouchRelease(); while (true) { + if (checkMenuTimeout()) break; autoDim(); uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); // 1. Touch on pattern selection (y: 0 to 122) if (ty < 122) { int8_t col = (tx < 160) ? 0 : 1; @@ -478,10 +505,11 @@ void beepSoundMenu() { break; } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); delay(200); break; } - yield(); + delay(25); } } @@ -517,40 +545,68 @@ void enterLightSleep() { digitalWrite(LED_GREEN_PIN, HIGH); digitalWrite(LED_BLUE_PIN, HIGH); - // Cleanly disconnect and terminate Bluetooth before sleep to release remote link + systemSleeping = true; // Signal background task to pause Bluetooth attempts + + // Cleanly disconnect active Bluetooth link before sleep to release remote connection + Serial.println(">>> [SLEEP] Disconnecting BT link..."); + Serial.flush(); if (BTSerial.hasClient() || BTSerial.connected() || foundOBD2) { BTSerial.disconnect(); delay(50); } - BTSerial.end(); foundOBD2 = false; - // 3. Configure wakeup sources: - // GPIO 36: XPT2046 Touch IRQ (PENIRQ), pulls LOW when touched - // GPIO 27: Selector Button (SELECTOR_PIN), pulls LOW when pressed - esp_sleep_enable_ext0_wakeup(GPIO_NUM_36, 0); - gpio_wakeup_enable((gpio_num_t)SELECTOR_PIN, GPIO_INTR_LOW_LEVEL); - esp_sleep_enable_gpio_wakeup(); + // Make sure finger or button is completely released before entering sleep loop + waitForTouchRelease(); - // 4. Enter Light Sleep - esp_light_sleep_start(); +#ifdef SERIAL_DEBUG + Serial.println(">>> [SLEEP] Sleeping (display & LEDs off, waiting for touch/button)..."); + Serial.flush(); +#endif + + // Put GPS module to sleep (UBX backup mode - debe ser lo ultimo antes de entrar en reposo) + gpsSleep(); + + while (true) { + delay(40); // Yields to FreeRTOS, feeds watchdog, minimal CPU activity + + // Check if woken by physical selector button (debounced) + if (digitalRead(SELECTOR_PIN) == LOW) { + delay(20); + if (digitalRead(SELECTOR_PIN) == LOW) { +#ifdef SERIAL_DEBUG + Serial.println(">>> [SLEEP] Woken up by physical button!"); +#endif + break; + } + } + + // Check if woken by touchscreen (debounced) + uint16_t dummyX, dummyY; + if (getTouch(&dummyX, &dummyY)) { + delay(20); + if (getTouch(&dummyX, &dummyY)) { +#ifdef SERIAL_DEBUG + Serial.println(">>> [SLEEP] Woken up by touchscreen touch!"); +#endif + break; + } + } + } // === WAKE UP === - gpio_wakeup_disable((gpio_num_t)SELECTOR_PIN); - rtc_gpio_deinit(GPIO_NUM_36); - - // Restart Bluetooth controller stack fresh - BTSerial.begin("OBD2 Gauge", true); - - // Restore rear RGB LED status in selected theme color if connected - if (foundOBD2) { - updateRearLedTheme(); - } +#ifdef SERIAL_DEBUG + Serial.println(">>> [SLEEP] Successfully woke up from sleep!"); + Serial.flush(); +#endif // 5. Wake LCD controller tft.writecommand(0x11); // ST7789_SLPOUT delay(120); + // Wake up GPS module + gpsWake(); + // 6. Restore backlight smoothly according to current brightness setting (Auto LDR or Manual) uint8_t wakeTarget = 255; if (brightnessIndex > 0) { @@ -586,6 +642,12 @@ void enterLightSleep() { // 8. Wait for touch to be fully released waitForTouchRelease(); + systemSleeping = false; + foundOBD2 = false; + if (btTaskHandle != NULL) { + xTaskNotifyGive(btTaskHandle); + } + wokeFromSleep = true; } @@ -666,9 +728,11 @@ void sleepMenu() { long holdtimer = 0; while (true) { + if (checkMenuTimeout()) break; autoDim(); uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); // 1. Tap on Auto-Sleep Timeout Tiles (y: 88 to 154) if (ty >= 88 && ty <= 154) { int8_t touchedTile = (tx - 8) / 62; @@ -708,6 +772,7 @@ void sleepMenu() { } } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); if (!pressed) { pressed = true; holdtimer = millis(); @@ -733,6 +798,7 @@ void sleepMenu() { } } else { if (pressed) { + resetMenuActivity(); if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) { subSel = (subSel == 0) ? 1 : 0; drawSleepBottomButtons(subSel); @@ -742,7 +808,7 @@ void sleepMenu() { delay(150); } } - yield(); + delay(25); } } @@ -762,11 +828,7 @@ void restartConfirmationMenu() { tft.fillRoundRect(18, 50, 4, 92, 2, getThemePrimaryColor()); // Left accent pill tft.setTextColor(TFT_WHITE, getThemeDarkColor()); - tft.drawCentreString("Desea reiniciar la placa?", 160, 56, 4); - - tft.setTextColor(0xBDD7, getThemeDarkColor()); - tft.drawCentreString("El sistema se reiniciara", 160, 92, 2); - tft.drawCentreString("y volvera a conectar con el OBD2", 160, 112, 2); + tft.drawCentreString("Desea reiniciar la placa?", 160, 80, 4); uint8_t subSel = 1; // 0: REINICIAR, 1: SALIR (por defecto SALIR para seguridad) @@ -802,9 +864,11 @@ void restartConfirmationMenu() { long holdtimer = 0; while (true) { + if (checkMenuTimeout()) break; autoDim(); uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); if (ty >= 155 && ty <= 225) { if (tx >= 10 && tx <= 158) { // Confirmado: Reiniciar! @@ -827,6 +891,7 @@ void restartConfirmationMenu() { } } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); if (!pressed) { pressed = true; holdtimer = millis(); @@ -850,6 +915,7 @@ void restartConfirmationMenu() { } } else { if (pressed) { + resetMenuActivity(); if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) { subSel = (subSel == 0) ? 1 : 0; drawSubButtons(subSel); @@ -859,7 +925,7 @@ void restartConfirmationMenu() { delay(150); } } - yield(); + delay(25); } } @@ -952,10 +1018,12 @@ void brightnessMenu() { waitForTouchRelease(); while (true) { + if (checkMenuTimeout()) break; autoDim(); uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); // 1. Direct Touch on Brightness Tiles (y: 106 to 176) if (ty >= 106 && ty < 176) { int8_t touchedTile = (tx - 7) / 52; @@ -976,6 +1044,7 @@ void brightnessMenu() { break; } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); if (!pressed) { pressed = true; holdtimer = millis(); @@ -988,6 +1057,7 @@ void brightnessMenu() { } } else { if (pressed) { + resetMenuActivity(); if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) { curChoice = (curChoice + 1) % 6; brightnessIndex = curChoice; @@ -999,7 +1069,7 @@ void brightnessMenu() { delay(150); } } - yield(); + delay(25); } } @@ -1060,11 +1130,13 @@ void soundsMenu() { waitForTouchRelease(); while (true) { + if (checkMenuTimeout()) break; autoDim(); int8_t executeSelect = -1; uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); if (ty >= 16 && ty < 96) { executeSelect = 0; } else if (ty >= 96 && ty < 176) { @@ -1085,6 +1157,7 @@ void soundsMenu() { } } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); if (!pressed) { pressed = true; holdtimer = millis(); @@ -1096,6 +1169,7 @@ void soundsMenu() { } } else { if (pressed) { + resetMenuActivity(); if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1200) { if (choice == 255) choice = 0; else choice = (choice + 1) % 3; @@ -1110,11 +1184,13 @@ void soundsMenu() { if (executeSelect >= 0) { if (executeSelect == 0) { alarmSoundMenu(); + if (menuExitedDueToTimeout) break; choice = 255; drawSoundsUI(choice); waitForTouchRelease(); } else if (executeSelect == 1) { beepSoundMenu(); + if (menuExitedDueToTimeout) break; choice = 255; drawSoundsUI(choice); waitForTouchRelease(); @@ -1124,7 +1200,7 @@ void soundsMenu() { } pressed = false; } - yield(); + delay(25); } } @@ -1135,7 +1211,7 @@ void drawThemeHeader() { tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor()); tft.fillRect(0, 26, 320, 2, getThemePrimaryColor()); tft.setTextColor(TFT_WHITE); - tft.drawCentreString("Apariencia / Temas", 160, 2, 4); + tft.drawCentreString("Temas de Color", 160, 2, 4); } void themeMenu() { @@ -1226,10 +1302,12 @@ void themeMenu() { long holdtimer = 0; while (true) { + if (checkMenuTimeout()) break; autoDim(); uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); // 1. Touch on theme tiles (y: 100 to 180) if (ty >= 100 && ty < 182) { uint8_t col = (tx < 110) ? 0 : ((tx < 212) ? 1 : 2); @@ -1247,12 +1325,13 @@ void themeMenu() { } waitForTouchRelease(); } - // 2. Touch on [ SALIR ] button (y: 182 to 240) - else if (ty >= 182 && tx >= 20 && tx <= 300) { + // 2. Touch on [ SALIR ] button (y: 178 to 240) + else if (ty >= 178 && tx >= 10 && tx <= 310) { animateExitButtonPress(); break; } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); if (!pressed) { pressed = true; holdtimer = millis(); @@ -1262,6 +1341,7 @@ void themeMenu() { } } else { if (pressed) { + resetMenuActivity(); if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) { themeIndex = (themeIndex + 1) % 6; pref.begin("setting", false); @@ -1274,7 +1354,312 @@ void themeMenu() { delay(150); } } - yield(); + delay(25); + } +} + +// ------------------------------------------------------------- +// Sub-screen 4: Menu de Transiciones (Deslizamiento / Desvanecimiento) +// ------------------------------------------------------------- +void drawTransitionHeader() { + tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor()); + tft.fillRect(0, 26, 320, 2, getThemePrimaryColor()); + tft.setTextColor(TFT_WHITE); + tft.drawCentreString("Efecto de Transicion", 160, 2, 4); +} + +void transitionMenu() { + tft.fillScreen(TFT_BLACK); + + auto drawTransitionUI = [&]() { + drawTransitionHeader(); + + const char* titles[2] = { "Deslizamiento", "Desvanecimiento" }; + const char* descs[2] = { "Barrido lateral continuo", "Mezcla gradual de pantallas" }; + + for (uint8_t i = 0; i < 2; i++) { + int16_t y = 36 + i * 54; + int16_t x = 12; + int16_t w = 296; + int16_t h = 48; + bool isSel = (i == transitionType); + + uint16_t bg = isSel ? getThemeDarkColor() : 0x1082; + uint16_t border = isSel ? getThemePrimaryColor() : 0x2104; + + tft.fillRoundRect(x, y, w, h, 6, bg); + tft.drawRoundRect(x, y, w, h, 6, border); + if (isSel) { + tft.drawRoundRect(x + 1, y + 1, w - 2, h - 2, 5, TFT_WHITE); + tft.fillRoundRect(x + 2, y + 4, 4, h - 8, 2, getThemePrimaryColor()); + } + + // Circular radio indicator + int16_t cx = x + 24; + int16_t cy = y + h / 2; + tft.drawCircle(cx, cy, 9, isSel ? TFT_WHITE : 0x4208); + if (isSel) { + tft.fillCircle(cx, cy, 6, getThemePrimaryColor()); + } else { + tft.fillCircle(cx, cy, 6, TFT_BLACK); + } + + // Title & Subtitle + tft.setTextColor(isSel ? TFT_WHITE : 0xBDD7, bg); + tft.drawString(titles[i], x + 44, y + 7, 4); + tft.setTextColor(isSel ? getThemePrimaryColor() : 0x8410, bg); + tft.drawString(descs[i], x + 44, y + 29, 2); + } + + // Interactive Preview Button [ PROBAR EFECTO ] + int16_t px = 60; + int16_t py = 146; + int16_t pw = 200; + int16_t ph = 32; + tft.fillRoundRect(px, py, pw, ph, 5, getThemeDarkColor()); + tft.drawRoundRect(px, py, pw, ph, 5, getThemePrimaryColor()); + tft.setTextColor(getThemePrimaryColor(), getThemeDarkColor()); + tft.drawCentreString("PROBAR EFECTO", 160, py + 7, 2); + + // Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42) + drawExitButton(false); + }; + + drawTransitionUI(); + waitForTouchRelease(); + + bool pressed = false; + long holdtimer = 0; + + while (true) { + if (checkMenuTimeout()) break; + autoDim(); + uint16_t tx = 0, ty = 0; + if (getTouch(&tx, &ty)) { + resetMenuActivity(); + // 1. Touch on option cards (y: 36 to 142) + if (ty >= 36 && ty < 90) { + if (transitionType != 0) { + transitionType = 0; + pref.begin("setting", false); + pref.putUChar("trans_type", transitionType); + pref.end(); + touchBeep(); + drawTransitionUI(); + } + waitForTouchRelease(); + } else if (ty >= 90 && ty < 144) { + if (transitionType != 1) { + transitionType = 1; + pref.begin("setting", false); + pref.putUChar("trans_type", transitionType); + pref.end(); + touchBeep(); + drawTransitionUI(); + } + waitForTouchRelease(); + } + // 2. Touch on [ PROBAR EFECTO ] (y: 142 to 176) + else if (ty >= 142 && ty <= 176 && tx >= 40 && tx <= 280) { + touchBeep(); + // Demonstrate transition across all 4 pages (Page 0 -> Page 1 -> Page 2 -> Page 3 -> Page 0) + performScreenTransition(true, 0, 1); + delay(350); + performScreenTransition(true, 1, 2); + delay(350); + performScreenTransition(true, 2, 3); + delay(350); + performScreenTransition(false, 3, 0); + delay(250); + tft.fillScreen(TFT_BLACK); + drawTransitionUI(); + waitForTouchRelease(); + } + // 3. Bottom button [ SALIR ] (y: 178 to 240) + else if (ty >= 178 && tx >= 10 && tx <= 310) { + pref.begin("setting", false); + pref.putUChar("trans_type", transitionType); + pref.end(); + animateExitButtonPress(); + break; + } + } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); + if (!pressed) { + pressed = true; + holdtimer = millis(); + } else if (millis() - holdtimer > 1000) { + // Long press on selector button: Exit and save + pref.begin("setting", false); + pref.putUChar("trans_type", transitionType); + pref.end(); + animateExitButtonPress(); + break; + } + } else { + if (pressed) { + resetMenuActivity(); + if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) { + transitionType = (transitionType == 0) ? 1 : 0; + pref.begin("setting", false); + pref.putUChar("trans_type", transitionType); + pref.end(); + touchBeep(); + drawTransitionUI(); + } + pressed = false; + delay(150); + } + } + delay(25); + } +} + +// ------------------------------------------------------------- +// Sub-screen 3: Menu de Apariencia (Temas de Color y Transiciones) +// ------------------------------------------------------------- +void drawAppearanceHeader() { + tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor()); + tft.fillRect(0, 26, 320, 2, getThemePrimaryColor()); + tft.setTextColor(TFT_WHITE); + tft.drawCentreString("Apariencia", 160, 2, 4); +} + +void appearanceMenu() { + uint8_t choice = 255; + bool pressed = false; + long holdtimer = 0; + + auto drawAppearanceUI = [&](uint8_t sel) { + tft.fillScreen(TFT_BLACK); + + drawAppearanceHeader(); + + // Card 0: Temas de Color (y: 34 to 102, h: 68) + uint16_t bg0 = (sel == 0) ? getThemeAccentColor() : getThemeDarkColor(); + uint16_t border0 = (sel == 0) ? getThemePrimaryColor() : getThemeAccentColor(); + tft.fillRoundRect(10, 34, 300, 68, 5, bg0); + tft.drawRoundRect(10, 34, 300, 68, 5, border0); + if (sel == 0) { + tft.drawRoundRect(11, 35, 298, 66, 4, TFT_WHITE); + } else { + tft.fillRoundRect(13, 40, 4, 56, 2, getThemePrimaryColor()); + } + tft.setTextColor(TFT_WHITE, bg0); + tft.drawString("Temas de Color", 24, 42, 4); + String themeInfo = String(GAUGE_THEMES[themeIndex].name) + " | " + String(GAUGE_THEMES[themeIndex].desc); + tft.setTextColor((sel == 0) ? TFT_YELLOW : 0xBDD7, bg0); + tft.drawString(themeInfo, 24, 72, 2); + tft.fillTriangle(296, 68, 290, 62, 290, 74, (sel == 0) ? TFT_WHITE : getThemePrimaryColor()); + + // Card 1: Efecto de Transicion (y: 110 to 178, h: 68) + uint16_t bg1 = (sel == 1) ? getThemeAccentColor() : getThemeDarkColor(); + uint16_t border1 = (sel == 1) ? getThemePrimaryColor() : getThemeAccentColor(); + tft.fillRoundRect(10, 110, 300, 68, 5, bg1); + tft.drawRoundRect(10, 110, 300, 68, 5, border1); + if (sel == 1) { + tft.drawRoundRect(11, 111, 298, 66, 4, TFT_WHITE); + } else { + tft.fillRoundRect(13, 116, 4, 56, 2, getThemePrimaryColor()); + } + tft.setTextColor(TFT_WHITE, bg1); + tft.drawString("Efecto Transicion", 24, 118, 4); + String transInfo = (transitionType == 0) ? "Deslizamiento | Barrido lateral" : "Desvanecimiento | Mezcla gradual"; + tft.setTextColor((sel == 1) ? TFT_YELLOW : 0xBDD7, bg1); + tft.drawString(transInfo, 24, 148, 2); + tft.fillTriangle(296, 144, 290, 138, 290, 150, (sel == 1) ? TFT_WHITE : getThemePrimaryColor()); + + // Bottom action button: [ SALIR ] (x: 80, y: 186, w: 160, h: 42) + drawExitButton(false); + if (sel == 2) { + tft.drawRoundRect(80, 186, 160, 42, 5, getThemePrimaryColor()); + tft.drawRoundRect(81, 187, 158, 40, 4, TFT_WHITE); + } + }; + + drawAppearanceUI(choice); + waitForTouchRelease(); + + while (true) { + if (checkMenuTimeout()) break; + autoDim(); + int8_t executeSelect = -1; + uint16_t tx = 0, ty = 0; + + if (getTouch(&tx, &ty)) { + resetMenuActivity(); + // 1. Touch on Card 0: Temas de Color (y: 30 to 104) + if (ty >= 30 && ty <= 104 && tx >= 10 && tx <= 310) { + executeSelect = 0; + } + // 2. Touch on Card 1: Efecto Transición (y: 106 to 176) + else if (ty >= 106 && ty <= 176 && tx >= 10 && tx <= 310) { + executeSelect = 1; + } + // 3. Touch on [ SALIR ] (y: 178 to 240) + else if (ty >= 178 && ty <= 240 && tx >= 10 && tx <= 310) { + executeSelect = 2; + } + + if (executeSelect >= 0) { + choice = executeSelect; + if (executeSelect == 2) { + animateExitButtonPress(); + break; + } else { + drawAppearanceUI(choice); + touchBeep(); + waitForTouchRelease(); + } + } + } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); + if (!pressed) { + pressed = true; + holdtimer = millis(); + } else if (millis() - holdtimer > 1000) { + if (choice != 255) { + executeSelect = choice; + } + pressed = false; + } + } else { + if (pressed) { + resetMenuActivity(); + if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1000) { + if (choice == 255) { + choice = 0; + } else { + choice++; + if (choice > 2) choice = 0; + } + drawAppearanceUI(choice); + touchBeep(); + } + pressed = false; + delay(150); + } + } + + if (executeSelect >= 0) { + if (executeSelect == 2) { + animateExitButtonPress(); + break; + } else if (executeSelect == 0) { + themeMenu(); + if (menuExitedDueToTimeout) break; + choice = 255; + drawAppearanceUI(choice); + waitForTouchRelease(); + } else if (executeSelect == 1) { + transitionMenu(); + if (menuExitedDueToTimeout) break; + choice = 255; + drawAppearanceUI(choice); + waitForTouchRelease(); + } + } + delay(25); } } @@ -1318,7 +1703,7 @@ void drawScrollDownButton(bool canScroll, bool pressed) { } // ------------------------------------------------------------- -// Menu Principal con Scroll Lateral (Opcion 3) +// Menu Principal con Scroll Lateral // ------------------------------------------------------------- void listMenu(uint8_t choice, uint8_t scrollOffset) { // Clear list area, scroll column, and bottom area (y: 28 to 240) @@ -1327,7 +1712,7 @@ void listMenu(uint8_t choice, uint8_t scrollOffset) { // 1. Draw 3 visible cards from menuList[scrollOffset .. scrollOffset + 2] for (uint8_t i = 0; i < 3; i++) { uint8_t itemIdx = scrollOffset + i; - if (itemIdx >= 6) break; + if (itemIdx >= 6) break; // 6 list items (0 to 5), item 6 is Salir int y = 32 + i * 50; bool isSel = (choice == itemIdx); @@ -1352,7 +1737,7 @@ void listMenu(uint8_t choice, uint8_t scrollOffset) { } // 2. Right Scroll Column (x: 268 to 310, w: 42) - uint8_t maxScroll = 3; // 6 total items - 3 visible items = 3 + uint8_t maxScroll = 3; // 6 total list items - 3 visible items = 3 // Up Button [ ▲ ] (x: 268, y: 32, w: 42, h: 42) drawScrollUpButton(scrollOffset > 0, false); @@ -1377,36 +1762,40 @@ void listMenu(uint8_t choice, uint8_t scrollOffset) { void configMenu() { uint8_t select = 255; uint8_t scrollOffset = 0; - const uint8_t maxScroll = 3; // 6 items - 3 visible = 3 + const uint8_t maxScroll = 3; // 6 list items - 3 visible = 3 bool pressed = false; long holdtimer = 0; + resetMenuActivity(); + menuExitedDueToTimeout = false; + tft.fillScreen(TFT_BLACK); drawConfigHeader(); listMenu(select, scrollOffset); waitForTouchRelease(); while (true) { - if (wokeFromSleep) break; + if (wokeFromSleep || checkMenuTimeout()) break; autoDim(); int8_t executeSelect = -1; uint16_t tx = 0, ty = 0; if (getTouch(&tx, &ty)) { + resetMenuActivity(); int8_t touchedOpt = -1; - // 1. Touch on bottom [ SALIR ] button (x: 10 to 264, y: 182 to 240) - if (ty >= 182 && ty <= 240 && tx >= 10 && tx < 265) { + // 1. Touch on bottom [ SALIR ] button (x: 10 to 264, y: 178 to 240) + if (ty >= 178 && ty <= 240 && tx >= 10 && tx < 265) { touchedOpt = 6; // SALIR } - // 2. Touch on the 3 visible cards (x: 10 to 264, y: 28 to 182) + // 2. Touch on the 3 visible cards (x: 10 to 264, y: 28 to 176) else if (tx >= 10 && tx < 265) { if (ty >= 28 && ty < 78) { touchedOpt = scrollOffset + 0; - } else if (ty >= 78 && ty < 129) { + } else if (ty >= 78 && ty < 128) { touchedOpt = scrollOffset + 1; - } else if (ty >= 129 && ty < 182) { + } else if (ty >= 128 && ty <= 176) { touchedOpt = scrollOffset + 2; } } @@ -1464,6 +1853,7 @@ void configMenu() { } } } else if (digitalRead(SELECTOR_PIN) == LOW) { + resetMenuActivity(); if (!pressed) { pressed = true; holdtimer = millis(); @@ -1475,6 +1865,7 @@ void configMenu() { } } else { if (pressed) { + resetMenuActivity(); if (millis() - holdtimer >= 40 && millis() - holdtimer <= 1200) { if (select == 255) { select = scrollOffset; @@ -1504,6 +1895,7 @@ void configMenu() { // Option 0: Alarmas (ON/OFF) if (select == 0) { warningMenu(); + if (menuExitedDueToTimeout) break; tft.fillScreen(TFT_BLACK); drawConfigHeader(); select = 255; @@ -1513,6 +1905,7 @@ void configMenu() { // Option 1: Sonidos (Menu de Sonido de Alarma y Sonido de Click) else if (select == 1) { soundsMenu(); + if (menuExitedDueToTimeout) break; tft.fillScreen(TFT_BLACK); drawConfigHeader(); select = 255; @@ -1522,15 +1915,17 @@ void configMenu() { // Option 2: Brillo de Pantalla else if (select == 2) { brightnessMenu(); + if (menuExitedDueToTimeout) break; tft.fillScreen(TFT_BLACK); drawConfigHeader(); select = 255; listMenu(select, scrollOffset); waitForTouchRelease(); } - // Option 3: Apariencia / Temas + // Option 3: Apariencia (Temas y Transiciones) else if (select == 3) { - themeMenu(); + appearanceMenu(); + if (menuExitedDueToTimeout) break; tft.fillScreen(TFT_BLACK); drawConfigHeader(); select = 255; @@ -1540,6 +1935,7 @@ void configMenu() { // Option 4: Modo Dormir else if (select == 4) { sleepMenu(); + if (wokeFromSleep || menuExitedDueToTimeout) break; tft.fillScreen(TFT_BLACK); drawConfigHeader(); select = 255; @@ -1549,6 +1945,7 @@ void configMenu() { // Option 5: Reiniciar else if (select == 5) { restartConfirmationMenu(); + if (menuExitedDueToTimeout) break; tft.fillScreen(TFT_BLACK); drawConfigHeader(); select = 255; @@ -1564,7 +1961,7 @@ void configMenu() { pressed = false; prompt = true; } - delay(30); + delay(25); } tft.fillScreen(TFT_BLACK); } diff --git a/sketch/obd2_gauge/gps_clock.h b/sketch/obd2_gauge/gps_clock.h new file mode 100644 index 0000000..a751773 --- /dev/null +++ b/sketch/obd2_gauge/gps_clock.h @@ -0,0 +1,393 @@ +#pragma once + +#include "DSEG7_Modern_BoldItalic_34.h" +#include "DSEG7_Modern_BoldItalic_80.h" +#include "Latin_Sans_Bold_26.h" +#include "gps_module.h" +#include +#include + +extern TFT_eSPI tft; +extern uint16_t getThemePrimaryColor(); +extern uint16_t getThemeBgColor(); +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; + +// 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 priCol = getThemePrimaryColor(); + 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, priCol); + + // --- 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 Primary Color) + tft.fillRoundRect(bx, by, bw, 24, 6, priCol); + tft.fillRect(bx, by + 18, bw, 6, priCol); // flatten lower header corners + tft.setTextColor(TFT_BLACK, priCol); + 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; + + tft.loadFont(Latin_Sans_Bold_26); + tft.setTextPadding(0); + + // Day of week in Latin Sans Bold 26 (Smooth antialiased, White, uppercase) + tft.setTextColor(TFT_WHITE, cardBg); + tft.drawString(SPANISH_DAYS[wday], tx, cardY + 22, 1); + + // Elegant two-tone horizontal dividing line + tft.drawFastHLine(tx, cardY + 54, 180, cardBorder); + tft.drawFastHLine(tx, cardY + 54, 50, priCol); + + // Year in Latin Sans Bold 26 (Smooth antialiased, Primary color) + tft.setTextColor(priCol, 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 +inline void draw7SegDot(TFT_eSPI &dst, int x0, int y0, uint16_t color, + uint16_t bgCol, int clip_h = 240) { + 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; + } + dst.drawPixel(curX, curY, dst.alphaBlend(a, color, bgCol)); + } + } + if (fl) { + dst.drawFastHLine(fxs, curY, fl, color); + } + } + dst.endWrite(); +} + +inline void draw7SegColon(TFT_eSPI &dst, int clkX, int clkY, uint16_t color, + uint16_t bgCol = 0x10A2, int clip_h = 240) { + // Top dot: centered at clkX + 156.5, font row 19..29 (y = clkY + 30) + draw7SegDot(dst, clkX + 151, clkY + 30, color, bgCol, clip_h); + // Bottom dot: centered at clkX + 153.5, font row 53..63 (y = clkY + 64) + draw7SegDot(dst, clkX + 148, clkY + 64, color, bgCol, clip_h); +} + +// 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 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, priCol); + + // Digits: Hours HH (Right aligned to clkX + 142) & Minutes MM (Left aligned + // to clkX + 170) + String hStr = (gpsHour < 10) ? "0" + String(gpsHour) : String(gpsHour); + String mStr = (gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute); + + tft.loadFont(DSEG7_Modern_BoldItalic_80); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(0); + tft.drawRightString(hStr.c_str(), clkX + 142, clkY + 11, 1); + tft.drawString(mStr.c_str(), clkX + 170, clkY + 11, 1); + tft.unloadFont(); + + // Blinking Colon Dots between HH and MM (italic 7-segment style with + // anti-aliasing) + uint16_t colonCol = (gpsSecond % 2 == 0) ? priCol : 0x18C3; + draw7SegColon(tft, clkX, clkY, colonCol, cardBg); + + // 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 cardBg = 0x10A2; + int clkX = 4; + int clkY = 19; + + // 1. Seconds change: update colon dots only (no seconds text) + if (forceRedraw || gpsSecond != oldClockSec) { + oldClockSec = gpsSecond; + + // Colon dots (italic 7-segment style with anti-aliasing) + uint16_t colonCol = (gpsSecond % 2 == 0) ? priCol : 0x18C3; + draw7SegColon(tft, clkX, clkY, colonCol, cardBg); + } + + // 2. Minutes change: update MM digits + if (forceRedraw || gpsMinute != oldClockMin) { + oldClockMin = gpsMinute; + tft.fillRect(clkX + 166, clkY + 10, 142, 82, cardBg); + String mStr = + (gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute); + tft.loadFont(DSEG7_Modern_BoldItalic_80); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(0); + tft.drawString(mStr.c_str(), clkX + 170, clkY + 11, 1); + tft.unloadFont(); + } + + // 3. Hours change: update HH digits + if (forceRedraw || gpsHour != oldClockHour) { + oldClockHour = gpsHour; + tft.fillRect(clkX + 12, clkY + 10, 136, 82, cardBg); + String hStr = (gpsHour < 10) ? "0" + String(gpsHour) : String(gpsHour); + tft.loadFont(DSEG7_Modern_BoldItalic_80); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(0); + tft.drawRightString(hStr.c_str(), clkX + 142, clkY + 11, 1); + tft.unloadFont(); + } + + // 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 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, priCol); + + // Colon dots during transition (italic 7-segment style with anti-aliasing) + draw7SegColon(s, clkX, clkY, priCol, cardBg, slice_h); + + // Digits: Hours HH (Right aligned to clkX + 142) & Minutes MM (Left aligned + // to clkX + 170) + String hStr = (gpsHour < 10) ? "0" + String(gpsHour) : String(gpsHour); + String mStr = + (gpsMinute < 10) ? "0" + String(gpsMinute) : String(gpsMinute); + + int digitY = clkY + 11; + if (digitY + 75 > 0 && digitY < slice_h) { + s.setTextWrap(false, false); + s.setTextColor(priCol, cardBg); + s.setTextPadding(0); + s.drawRightString(hStr.c_str(), clkX + 142, digitY, 8); + s.drawString(mStr.c_str(), clkX + 170, digitY, 8); + } + } + + // 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) { + 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, priCol); + + // 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, priCol); + s.fillRect(bx, by + 18, bw, 6, priCol); + uint8_t mIdx = (gpsMonth >= 1 && gpsMonth <= 12) ? (gpsMonth - 1) : 0; + s.setTextColor(TFT_BLACK, priCol); + 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) { + 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, priCol); + } + + int yrY = dateY + 64; + if (yrY + 26 > 0 && yrY < slice_h) { + s.setTextColor(priCol, cardBg); + s.drawString(String(gpsYear).c_str(), tx, yrY, 4); + } + } +} diff --git a/sketch/obd2_gauge/gps_icon_aa_data.h b/sketch/obd2_gauge/gps_icon_aa_data.h new file mode 100644 index 0000000..c68f13b --- /dev/null +++ b/sketch/obd2_gauge/gps_icon_aa_data.h @@ -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 + +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, +}; diff --git a/sketch/obd2_gauge/gps_module.h b/sketch/obd2_gauge/gps_module.h new file mode 100644 index 0000000..88d335b --- /dev/null +++ b/sketch/obd2_gauge/gps_module.h @@ -0,0 +1,625 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#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 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 +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 +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); + } + } +} + diff --git a/sketch/obd2_gauge/gps_nav.h b/sketch/obd2_gauge/gps_nav.h new file mode 100644 index 0000000..443e4db --- /dev/null +++ b/sketch/obd2_gauge/gps_nav.h @@ -0,0 +1,502 @@ +#pragma once + +#include +#include +#include "gps_module.h" +#include "DSEG7_Modern_BoldItalic_34.h" + +extern TFT_eSPI tft; +extern uint16_t getThemePrimaryColor(); +extern uint16_t getThemeBgColor(); +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 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; + 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 cardBg = 0x10A2; // Dark graphite navy + uint16_t cardBorder = 0x2945; // Subtle edge border + bool synced = isGpsSynced(); + + // 1. TOP BAR (y = 0 to 18) + // Left: Screen Title + tft.setTextColor(0x8410, TFT_BLACK); + tft.drawString("TELEMETRIA GPS", 8 + x_offset, 5, 1); + + // 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, priCol); + + 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)) : "---"; + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(0); + tft.drawRightString(altStr.c_str(), c0X + 140, c0Y + 28, 1); + tft.unloadFont(); + + // 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, priCol); + + tft.setTextColor(0x8410, cardBg); + tft.drawString("RUMBO / BRUJULA", c1X + 16, c1Y + 8, 2); + + bool hasCourse = synced && isGpsCourseValid(); + String crsStr = hasCourse ? String((int)round(gpsCourseDeg)) : "---"; + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(0); + tft.drawRightString(crsStr.c_str(), c1X + 130, c1Y + 28, 1); + tft.unloadFont(); + if (hasCourse) { + tft.drawCircle(c1X + 137, c1Y + 33, 3, priCol); + tft.drawCircle(c1X + 137, c1Y + 33, 2, priCol); + } 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, priCol); + + 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, priCol, waveState, cardBg); + + // Large Satellites Count (DSEG7 34pt) + String satStr = String(gpsSats); + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(0); + tft.drawRightString(satStr.c_str(), c2X + 140, c2Y + 26, 1); + tft.unloadFont(); + + // 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, priCol); + + 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(priCol, 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(priCol, 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 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) : "---"; + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(126); + tft.drawRightString(altStr.c_str(), c0X + 140, c0Y + 28, 1); + tft.setTextPadding(0); + tft.unloadFont(); + } + + // 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) : "---"; + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(85); + tft.drawRightString(crsStr.c_str(), c1X + 130, c1Y + 28, 1); + tft.setTextPadding(0); + tft.unloadFont(); + + if (hasCourse) { + tft.drawCircle(c1X + 137, c1Y + 33, 3, priCol); + tft.drawCircle(c1X + 137, c1Y + 33, 2, priCol); + } 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) + if (forceRedraw || gpsSats != lastNavCardSats || synced != lastNavSatsSync) { + lastNavCardSats = gpsSats; + lastNavSatsSync = synced; + oldNavSats = gpsSats; + String satStr = String(gpsSats); + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(priCol, cardBg); + tft.setTextPadding(85); + tft.drawRightString(satStr.c_str(), c2X + 140, c2Y + 26, 1); + tft.setTextPadding(0); + tft.unloadFont(); + } + + // 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 : priCol, 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, priCol, 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(priCol, 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 cardBg = 0x10A2; + uint16_t cardBorder = 0x2945; + bool synced = isGpsSynced(); + + // 1. TOP BAR in top slice + if (y_slice == 0) { + s.setTextColor(0x8410, TFT_BLACK); + s.drawString("TELEMETRIA GPS", 8 + x_offset, 5, 1); + } + + // 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, priCol); + + 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) { + s.fontLoaded = true; + s.setTextColor(priCol, cardBg); + s.setTextPadding(0); + s.drawRightString(altStr.c_str(), c0X + 140, valY, 1); + s.fontLoaded = false; + } + } + + // 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, priCol); + + 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) { + s.fontLoaded = true; + s.setTextColor(priCol, cardBg); + s.setTextPadding(0); + s.drawRightString(crsStr.c_str(), c1X + 130, valY, 1); + s.fontLoaded = false; + if (hasCourse) { + int circY = c1Y + 33; + if (circY + 4 > 0 && circY - 4 < slice_h) { + s.drawCircle(c1X + 137, circY, 3, priCol); + s.drawCircle(c1X + 137, circY, 2, priCol); + } + } + } + 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, priCol); + + 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, priCol, waveState, cardBg); + } + int satY = c2Y + 26; + if (satY < slice_h && satY + 38 > 0) { + String satStr = String(gpsSats); + s.fontLoaded = true; + s.setTextColor(priCol, cardBg); + s.setTextPadding(0); + s.drawRightString(satStr.c_str(), c2X + 140, satY, 1); + s.fontLoaded = false; + } + 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 : priCol, 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, priCol); + + 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(priCol, 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(priCol, cardBg); + s.drawString(lonStr.c_str(), c3X + 16, lonValY, 2); + } + } +} diff --git a/sketch/obd2_gauge/meter.h b/sketch/obd2_gauge/meter.h index fb8ae09..8596beb 100644 --- a/sketch/obd2_gauge/meter.h +++ b/sketch/obd2_gauge/meter.h @@ -1,8 +1,12 @@ // All Meters display function -#include "DSEG7_Modern_BoldItalic_80.h" -#include "DSEG7_Modern_BoldItalic_44.h" -#include "DSEG7_Modern_BoldItalic_34.h" #include "DSEG7_Modern_BoldItalic_24.h" +#include "DSEG7_Modern_BoldItalic_34.h" +#include "DSEG7_Modern_BoldItalic_44.h" +#include "DSEG7_Modern_BoldItalic_80.h" +#include "Latin_Sans_Bold_26.h" + +extern TFT_eSprite sprTrans; +extern bool sprTransAllocated; const uint16_t screenWidth = tft.height(); // landscape const uint16_t screenHeight = tft.width(); @@ -35,21 +39,25 @@ uint8_t pidReadSkip[maxpidIndex] = {}; // skip reading 0 - 3; 3 = max delay read uint8_t pidCurrentSkip[maxpidIndex] = {}; // current skip counter uint8_t engine_off_count = 0; // counter to indicate engine is off then sleep float old_data[maxpidIndex] = {-999, -999, -999, -999, -999}; // keep old data -float last_valid_data[maxpidIndex] = {-999, -999, -999, -999, -999}; // keep last valid data for instant redraw +float last_valid_data[maxpidIndex] = { + -999, -999, -999, -999, -999}; // keep last valid data for instant redraw float currentRpm = 0.0f; // Current engine RPM for sleep/inactivity tracking -float currentEngineLoad = 0.0f; // Current Engine Load % (PID 0104) -bool isEcoOptimal = false; // Flag: RPM 1900-2550 & Load 45-82% -bool badgeEcoDrawn = false; // Tracking rendered status on display +float currentEngineLoad = 0.0f; // Current Engine Load % (PID 0104) +bool isEcoOptimal = false; // Flag: RPM 1900-2550 & Load 45-82% +bool badgeEcoDrawn = false; // Tracking rendered status on display extern uint32_t lastActivityTime; // Activity timestamp for auto-sleep -bool ignitionOn = true; // True if ECU is actively communicating (contacto puesto) -uint8_t consecutiveEcuFails = 0; // Counter for consecutive ECU communication failures +bool ignitionOn = + true; // True if ECU is actively communicating (contacto puesto) +uint8_t consecutiveEcuFails = + 0; // Counter for consecutive ECU communication failures // Arc warning flash (blinking alarm when gauge is in TFT_RED) bool arcFlashVisible = true; uint32_t lastArcFlashTime = 0; -bool wasInRedAlarm[4] = { false, false, false, false }; +bool wasInRedAlarm[4] = {false, false, false, false}; -// Color for inactive meter blocks (medium-dark graphite grey, lighter than 0x2124 but darker than 0x7BEF) +// Color for inactive meter blocks (medium-dark graphite grey, lighter than +// 0x2124 but darker than 0x7BEF) const uint16_t METER_INACTIVE_COLOR = 0x39E7; // RGB ~ (58, 60, 58) // backlight variable average counter @@ -60,14 +68,16 @@ const float factoryECUOff = 12.5; // default off voltage float ecu_off_volt = factoryECUOff; // turn engine off if voltage lower // than 12.4 -// Cache theme gradients to ensure 100% bit-for-bit identical background restoration +// Cache theme gradients to ensure 100% bit-for-bit identical background +// restoration uint16_t cachedThemeColor = 0xFFFF; uint16_t themeGradQuad[82]; uint16_t themeGradHeader[145]; void checkUpdateThemeGradients() { uint16_t currentTheme = getThemeBgColor(); - if (currentTheme == cachedThemeColor) return; + if (currentTheme == cachedThemeColor) + return; cachedThemeColor = currentTheme; float delta = -255.0f / 82.0f; @@ -93,22 +103,30 @@ inline uint16_t getInstrumentBgColor(int y, bool isQuad, int ry = 0) { checkUpdateThemeGradients(); if (isQuad) { int k = y - ry; - if (k < 0) return themeGradQuad[0]; - if (k >= 82) return TFT_BLACK; + if (k < 0) + return themeGradQuad[0]; + if (k >= 82) + return TFT_BLACK; return themeGradQuad[k]; } else { - if (y < 0) return themeGradHeader[0]; - if (y >= 145) return TFT_BLACK; + if (y < 0) + return themeGradHeader[0]; + if (y >= 145) + return TFT_BLACK; return themeGradHeader[y]; } } -// Shared line/block buffer for fast SPI push (up to 64x64 max icon size, ~8KB static RAM) +// Shared line/block buffer for fast SPI push (up to 64x64 max icon size, ~8KB +// static RAM) static uint16_t s_iconRenderBuf[64 * 64]; -// Draw icon with true 8-bit alpha channel blended dynamically against current instrument background -void drawIconWithAlpha(int x, int y, int w, int h, const unsigned short *colors, const unsigned char *alphas, bool isQuad, int ry = 0) { - if (w * h > 64 * 64) return; +// Draw icon with true 8-bit alpha channel blended dynamically against current +// instrument background +void drawIconWithAlpha(int x, int y, int w, int h, const unsigned short *colors, + const unsigned char *alphas, bool isQuad, int ry = 0) { + if (w * h > 64 * 64) + return; for (int j = 0; j < h; j++) { int curY = y + j; @@ -134,17 +152,22 @@ void drawIconWithAlpha(int x, int y, int w, int h, const unsigned short *colors, void drawPidIcon(int x, int y, const String &pidStr) { int ry = (y >= 120) ? 121 : 0; if (pidStr == "0105") { - drawIconWithAlpha(x, y, 38, 37, icon_temp_quad, icon_temp_quad_alpha, true, ry); + drawIconWithAlpha(x, y, 38, 37, icon_temp_quad, icon_temp_quad_alpha, true, + ry); } else if (pidStr == "010D") { - drawIconWithAlpha(x, y, 42, 42, icon_speed_quad, icon_speed_quad_alpha, true, ry); + drawIconWithAlpha(x, y, 42, 42, icon_speed_quad, icon_speed_quad_alpha, + true, ry); } else if (pidStr == "010C") { - drawIconWithAlpha(x, y, 42, 43, icon_rpm_quad, icon_rpm_quad_alpha, true, ry); + drawIconWithAlpha(x, y, 42, 43, icon_rpm_quad, icon_rpm_quad_alpha, true, + ry); } else if (pidStr == "0142" || pidStr == "ATRV") { - drawIconWithAlpha(x, y, 46, 35, icon_battery_quad, icon_battery_quad_alpha, true, ry); + drawIconWithAlpha(x, y, 46, 35, icon_battery_quad, icon_battery_quad_alpha, + true, ry); } } -// Clear parameter icon for 4-quadrant view by restoring exact background lines (100% invisible restoration) +// Clear parameter icon for 4-quadrant view by restoring exact background lines +// (100% invisible restoration) void clearPidIcon(int x, int y, const String &pidStr) { int w = 42; int h = 42; @@ -184,11 +207,13 @@ void drawPidIconHeader(int x, int y, const String &pidStr) { } else if (pidStr == "010C") { drawIconWithAlpha(11, 4, 42, 43, icon_rpm_quad, icon_rpm_quad_alpha, false); } else if (pidStr == "0142" || pidStr == "ATRV") { - drawIconWithAlpha(8, 2, 48, 48, icon_battery_48, icon_battery_48_alpha, false); + drawIconWithAlpha(8, 2, 48, 48, icon_battery_48, icon_battery_48_alpha, + false); } } -// Restore ambient vignette background over the header icon area during alarm blink OFF phase (100% invisible) +// Restore ambient vignette background over the header icon area during alarm +// blink OFF phase (100% invisible) void clearPidIconHeader(int x = 8, int y = 2, int w = 50, int h = 52) { for (int j = 0; j < h; j++) { int curY = y + j; @@ -202,7 +227,8 @@ void clearPidIconHeader(int x = 8, int y = 2, int w = 50, int h = 52) { tft.pushImage(x, y, w, h, s_iconRenderBuf); } -// Draw or clear full screen instrument title during normal display and alarm blinking +// Draw or clear full screen instrument title during normal display and alarm +// blinking void drawFullScreenTitle(const String &label, uint16_t color) { checkUpdateThemeGradients(); for (int y = 10; y <= 40; y++) { @@ -215,8 +241,23 @@ void drawFullScreenTitle(const String &label, uint16_t color) { } } -// Tactical ECO / OPT Badge (Ford Ka Endura 1.3 Sweet Spot: 1900 - 2550 RPM & 45% - 82% Load) +// Tactical ECO / OPT Badge (Ford Ka Endura 1.3 Sweet Spot: 1900 - 2550 RPM & +// 45% - 82% Load) void updateEcoBadge(bool forceRedraw = false) { + // CRITICAL: ECO badge ONLY belongs on Page 0 (RPM quadrant or fullscreen + // RPM), NEVER on Page 1 (Tri-Temp)! + if (currentScreenPage != 0) { + badgeEcoDrawn = false; + return; + } + + // If in fullscreen on Page 0, only display if the fullscreen cell is RPM + bool isQuad = (fullScreenCell < 0 || fullScreenCell >= 4); + if (!isQuad && pidConfig[pidInCell[fullScreenCell]][3].toInt() != 3) { + badgeEcoDrawn = false; + return; + } + bool newEco = (ignitionOn && currentRpm >= 1900.0f && currentRpm <= 2550.0f && currentEngineLoad >= 45.0f && currentEngineLoad <= 82.0f); isEcoOptimal = newEco; @@ -229,7 +270,6 @@ void updateEcoBadge(bool forceRedraw = false) { return; } - bool isQuad = (fullScreenCell < 0 || fullScreenCell >= 4); int bx = 0, by = 0, bw = 0, bh = 0; if (isQuad) { @@ -277,80 +317,89 @@ void updateEcoBadge(bool forceRedraw = false) { badgeEcoDrawn = newEco; } -// Full Screen Exponential Segmented Block Bar Graph (20 blocks: 3mm width x 4mm height, 0.5mm gap, Anti-Aliased) +// Full Screen Exponential Segmented Block Bar Graph (20 blocks: 3mm width x 4mm +// height, 0.5mm gap, Anti-Aliased) const uint8_t NUM_BAR_BLOCKS = 20; -const int16_t blockMinX[20] PROGMEM = { 0, 5, 11, 17, 25, 34, 46, 59, 76, 95, 114, 134, 154, 174, 193, 213, 232, 251, 271, 290 }; -const int16_t blockMaxX[20] PROGMEM = { 28, 34, 40, 47, 55, 65, 76, 89, 104, 121, 138, 157, 175, 194, 214, 233, 252, 271, 290, 310 }; -const int16_t blockMinY[20] PROGMEM = { 202, 183, 164, 146, 127, 110, 94, 80, 70, 63, 59, 56, 54, 53, 52, 52, 52, 52, 52, 52 }; -const int16_t blockMaxY[20] PROGMEM = { 226, 208, 190, 172, 155, 139, 124, 111, 100, 92, 87, 83, 80, 79, 78, 77, 77, 77, 77, 77 }; +const int16_t blockMinX[20] PROGMEM = {0, 5, 11, 17, 25, 34, 46, + 59, 76, 95, 114, 134, 154, 174, + 193, 213, 232, 251, 271, 290}; +const int16_t blockMaxX[20] PROGMEM = {28, 34, 40, 47, 55, 65, 76, + 89, 104, 121, 138, 157, 175, 194, + 214, 233, 252, 271, 290, 310}; +const int16_t blockMinY[20] PROGMEM = {202, 183, 164, 146, 127, 110, 94, + 80, 70, 63, 59, 56, 54, 53, + 52, 52, 52, 52, 52, 52}; +const int16_t blockMaxY[20] PROGMEM = {226, 208, 190, 172, 155, 139, 124, + 111, 100, 92, 87, 83, 80, 79, + 78, 77, 77, 77, 77, 77}; const float blockNx[20][4] PROGMEM = { - { 0.96709f, -0.26991f, -0.96711f, 0.24260f }, - { 0.95775f, -0.30696f, -0.95776f, 0.26991f }, - { 0.94416f, -0.35424f, -0.94418f, 0.30696f }, - { 0.92359f, -0.41544f, -0.92365f, 0.35424f }, - { 0.89126f, -0.49508f, -0.89141f, 0.41544f }, - { 0.83905f, -0.59638f, -0.83936f, 0.49508f }, - { 0.75533f, -0.71496f, -0.75581f, 0.59638f }, - { 0.63154f, -0.83065f, -0.63179f, 0.71496f }, - { 0.47873f, -0.91631f, -0.47824f, 0.83065f }, - { 0.33039f, -0.96398f, -0.32962f, 0.91631f }, - { 0.21408f, -0.98563f, -0.21356f, 0.96398f }, - { 0.13436f, -0.99446f, -0.13412f, 0.98563f }, - { 0.08319f, -0.99789f, -0.08309f, 0.99446f }, - { 0.05123f, -0.99920f, -0.05120f, 0.99789f }, - { 0.03149f, -0.99970f, -0.03148f, 0.99920f }, - { 0.01934f, -0.99989f, -0.01934f, 0.99970f }, - { 0.01188f, -0.99996f, -0.01187f, 0.99989f }, - { 0.00729f, -0.99998f, -0.00729f, 0.99996f }, - { 0.00448f, -0.99999f, -0.00448f, 0.99998f }, - { 0.00275f, -1.00000f, -0.00275f, 0.99999f }, + {0.96709f, -0.26991f, -0.96711f, 0.24260f}, + {0.95775f, -0.30696f, -0.95776f, 0.26991f}, + {0.94416f, -0.35424f, -0.94418f, 0.30696f}, + {0.92359f, -0.41544f, -0.92365f, 0.35424f}, + {0.89126f, -0.49508f, -0.89141f, 0.41544f}, + {0.83905f, -0.59638f, -0.83936f, 0.49508f}, + {0.75533f, -0.71496f, -0.75581f, 0.59638f}, + {0.63154f, -0.83065f, -0.63179f, 0.71496f}, + {0.47873f, -0.91631f, -0.47824f, 0.83065f}, + {0.33039f, -0.96398f, -0.32962f, 0.91631f}, + {0.21408f, -0.98563f, -0.21356f, 0.96398f}, + {0.13436f, -0.99446f, -0.13412f, 0.98563f}, + {0.08319f, -0.99789f, -0.08309f, 0.99446f}, + {0.05123f, -0.99920f, -0.05120f, 0.99789f}, + {0.03149f, -0.99970f, -0.03148f, 0.99920f}, + {0.01934f, -0.99989f, -0.01934f, 0.99970f}, + {0.01188f, -0.99996f, -0.01187f, 0.99989f}, + {0.00729f, -0.99998f, -0.00729f, 0.99996f}, + {0.00448f, -0.99999f, -0.00448f, 0.99998f}, + {0.00275f, -1.00000f, -0.00275f, 0.99999f}, }; const float blockNy[20][4] PROGMEM = { - { 0.25444f, 0.96289f, -0.25437f, -0.97013f }, - { 0.28762f, 0.95172f, -0.28757f, -0.96289f }, - { 0.32950f, 0.93516f, -0.32942f, -0.95172f }, - { 0.38338f, 0.90962f, -0.38323f, -0.93516f }, - { 0.45348f, 0.86885f, -0.45320f, -0.90962f }, - { 0.54405f, 0.80270f, -0.54358f, -0.86885f }, - { 0.65535f, 0.69916f, -0.65480f, -0.80270f }, - { 0.77534f, 0.55679f, -0.77514f, -0.69916f }, - { 0.87796f, 0.40047f, -0.87823f, -0.55679f }, - { 0.94384f, 0.26596f, -0.94412f, -0.40047f }, - { 0.97682f, 0.16890f, -0.97693f, -0.26596f }, - { 0.99093f, 0.10510f, -0.99097f, -0.16890f }, - { 0.99653f, 0.06486f, -0.99654f, -0.10510f }, - { 0.99869f, 0.03990f, -0.99869f, -0.06486f }, - { 0.99950f, 0.02452f, -0.99950f, -0.03990f }, - { 0.99981f, 0.01506f, -0.99981f, -0.02452f }, - { 0.99993f, 0.00924f, -0.99993f, -0.01506f }, - { 0.99997f, 0.00568f, -0.99997f, -0.00924f }, - { 0.99999f, 0.00348f, -0.99999f, -0.00568f }, - { 1.00000f, 0.00223f, -1.00000f, -0.00348f }, + {0.25444f, 0.96289f, -0.25437f, -0.97013f}, + {0.28762f, 0.95172f, -0.28757f, -0.96289f}, + {0.32950f, 0.93516f, -0.32942f, -0.95172f}, + {0.38338f, 0.90962f, -0.38323f, -0.93516f}, + {0.45348f, 0.86885f, -0.45320f, -0.90962f}, + {0.54405f, 0.80270f, -0.54358f, -0.86885f}, + {0.65535f, 0.69916f, -0.65480f, -0.80270f}, + {0.77534f, 0.55679f, -0.77514f, -0.69916f}, + {0.87796f, 0.40047f, -0.87823f, -0.55679f}, + {0.94384f, 0.26596f, -0.94412f, -0.40047f}, + {0.97682f, 0.16890f, -0.97693f, -0.26596f}, + {0.99093f, 0.10510f, -0.99097f, -0.16890f}, + {0.99653f, 0.06486f, -0.99654f, -0.10510f}, + {0.99869f, 0.03990f, -0.99869f, -0.06486f}, + {0.99950f, 0.02452f, -0.99950f, -0.03990f}, + {0.99981f, 0.01506f, -0.99981f, -0.02452f}, + {0.99993f, 0.00924f, -0.99993f, -0.01506f}, + {0.99997f, 0.00568f, -0.99997f, -0.00924f}, + {0.99999f, 0.00348f, -0.99999f, -0.00568f}, + {1.00000f, 0.00223f, -1.00000f, -0.00348f}, }; const float blockC[20][4] PROGMEM = { - { -57.0917f, -194.0217f, 79.0747f, 212.4570f }, - { -63.7256f, -171.8745f, 85.7128f, 191.2717f }, - { -71.2233f, -148.4987f, 93.2024f, 169.1245f }, - { -79.6909f, -123.2275f, 101.6563f, 145.7487f }, - { -89.0635f, -95.0180f, 111.0070f, 120.4775f }, - { -98.7898f, -62.4921f, 120.7056f, 92.2680f }, - { -107.1996f, -24.6792f, 129.0998f, 59.7421f }, - { -111.1224f, 16.8839f, 133.0287f, 21.9292f }, - { -107.7811f, 57.2365f, 129.6718f, -19.6339f }, - { -98.3836f, 92.0853f, 120.2644f, -59.9865f }, - { -87.1827f, 121.1162f, 109.0982f, -94.8353f }, - { -77.3181f, 146.0701f, 99.2734f, -123.8662f }, - { -69.7527f, 168.5916f, 91.7322f, -148.8201f }, - { -64.3207f, 189.7308f, 86.3119f, -171.3416f }, - { -60.5502f, 210.0867f, 82.5466f, -192.4808f }, - { -57.9834f, 229.9930f, 79.9819f, -212.8367f }, - { -56.2577f, 249.6371f, 78.2571f, -232.7430f }, - { -55.1079f, 269.1261f, 77.1076f, -252.3871f }, - { -54.3470f, 288.5222f, 76.3469f, -271.8761f }, - { -53.8454f, 307.8565f, 75.8453f, -291.2722f }, + {-57.0917f, -194.0217f, 79.0747f, 212.4570f}, + {-63.7256f, -171.8745f, 85.7128f, 191.2717f}, + {-71.2233f, -148.4987f, 93.2024f, 169.1245f}, + {-79.6909f, -123.2275f, 101.6563f, 145.7487f}, + {-89.0635f, -95.0180f, 111.0070f, 120.4775f}, + {-98.7898f, -62.4921f, 120.7056f, 92.2680f}, + {-107.1996f, -24.6792f, 129.0998f, 59.7421f}, + {-111.1224f, 16.8839f, 133.0287f, 21.9292f}, + {-107.7811f, 57.2365f, 129.6718f, -19.6339f}, + {-98.3836f, 92.0853f, 120.2644f, -59.9865f}, + {-87.1827f, 121.1162f, 109.0982f, -94.8353f}, + {-77.3181f, 146.0701f, 99.2734f, -123.8662f}, + {-69.7527f, 168.5916f, 91.7322f, -148.8201f}, + {-64.3207f, 189.7308f, 86.3119f, -171.3416f}, + {-60.5502f, 210.0867f, 82.5466f, -192.4808f}, + {-57.9834f, 229.9930f, 79.9819f, -212.8367f}, + {-56.2577f, 249.6371f, 78.2571f, -232.7430f}, + {-55.1079f, 269.1261f, 77.1076f, -252.3871f}, + {-54.3470f, 288.5222f, 76.3469f, -271.8761f}, + {-53.8454f, 307.8565f, 75.8453f, -291.2722f}, }; // Draw an individual solid rectangular block with smooth subpixel anti-aliasing @@ -364,7 +413,7 @@ inline void drawBarBlock(uint8_t i, uint16_t color) { for (uint8_t k = 0; k < 4; k++) { nx[k] = pgm_read_float(&blockNx[i][k]); ny[k] = pgm_read_float(&blockNy[i][k]); - c[k] = pgm_read_float(&blockC[i][k]); + c[k] = pgm_read_float(&blockC[i][k]); } tft.startWrite(); @@ -375,9 +424,11 @@ inline void drawBarBlock(uint8_t i, uint16_t color) { float min_d = 999.0f; for (uint8_t k = 0; k < 4; k++) { float d = px * nx[k] + py * ny[k] + c[k]; - if (d < min_d) min_d = d; + if (d < min_d) + min_d = d; } - if (min_d <= -0.5f) continue; + if (min_d <= -0.5f) + continue; if (color == TFT_BLACK) { tft.drawPixel(x, y, TFT_BLACK); } else { @@ -393,82 +444,92 @@ inline void drawBarBlock(uint8_t i, uint16_t color) { tft.endWrite(); } -// 4-Quadrant Exponential Segmented Block Bar Graph (20 blocks per 160x120 quadrant, Anti-Aliased) -const int16_t quadBlockMinX[20] PROGMEM = { 5, 8, 12, 15, 20, 24, 30, 37, 45, 53, 62, 71, 80, 89, 98, 107, 117, 126, 135, 144 }; -const int16_t quadBlockMaxX[20] PROGMEM = { 18, 21, 24, 28, 33, 38, 44, 50, 58, 65, 74, 82, 91, 100, 109, 118, 127, 136, 145, 154 }; -const int16_t quadBlockMinY[20] PROGMEM = { 100, 91, 83, 74, 66, 59, 51, 45, 40, 36, 33, 31, 30, 29, 28, 28, 27, 27, 27, 27 }; -const int16_t quadBlockMaxY[20] PROGMEM = { 112, 103, 95, 87, 79, 72, 65, 59, 53, 49, 46, 43, 41, 40, 39, 38, 38, 37, 37, 37 }; +// 4-Quadrant Exponential Segmented Block Bar Graph (20 blocks per 160x120 +// quadrant, Anti-Aliased) +const int16_t quadBlockMinX[20] PROGMEM = {5, 8, 12, 15, 20, 24, 30, + 37, 45, 53, 62, 71, 80, 89, + 98, 107, 117, 126, 135, 144}; +const int16_t quadBlockMaxX[20] PROGMEM = {18, 21, 24, 28, 33, 38, 44, + 50, 58, 65, 74, 82, 91, 100, + 109, 118, 127, 136, 145, 154}; +const int16_t quadBlockMinY[20] PROGMEM = {100, 91, 83, 74, 66, 59, 51, + 45, 40, 36, 33, 31, 30, 29, + 28, 28, 27, 27, 27, 27}; +const int16_t quadBlockMaxY[20] PROGMEM = {112, 103, 95, 87, 79, 72, 65, + 59, 53, 49, 46, 43, 41, 40, + 39, 38, 38, 37, 37, 37}; const float quadBlockNx[20][4] PROGMEM = { - { -0.94779f, -0.31891f, 0.94779f, 0.31891f }, - { -0.93472f, -0.35539f, 0.93472f, 0.35539f }, - { -0.91662f, -0.39976f, 0.91662f, 0.39976f }, - { -0.89115f, -0.45372f, 0.89115f, 0.45372f }, - { -0.85466f, -0.51919f, 0.85466f, 0.51919f }, - { -0.80234f, -0.59687f, 0.80234f, 0.59687f }, - { -0.72909f, -0.68442f, 0.72909f, 0.68442f }, - { -0.63267f, -0.77442f, 0.63267f, 0.77442f }, - { -0.51899f, -0.85478f, 0.51899f, 0.85478f }, - { -0.40261f, -0.91537f, 0.40261f, 0.91537f }, - { -0.29860f, -0.95438f, 0.29860f, 0.95438f }, - { -0.21498f, -0.97662f, 0.21498f, 0.97662f }, - { -0.15215f, -0.98836f, 0.15215f, 0.98836f }, - { -0.10673f, -0.99429f, 0.10673f, 0.99429f }, - { -0.07452f, -0.99722f, 0.07452f, 0.99722f }, - { -0.05192f, -0.99865f, 0.05192f, 0.99865f }, - { -0.03612f, -0.99935f, 0.03612f, 0.99935f }, - { -0.02513f, -0.99968f, 0.02513f, 0.99968f }, - { -0.01747f, -0.99985f, 0.01747f, 0.99985f }, - { -0.01214f, -0.99993f, 0.01214f, 0.99993f }, + {-0.94779f, -0.31891f, 0.94779f, 0.31891f}, + {-0.93472f, -0.35539f, 0.93472f, 0.35539f}, + {-0.91662f, -0.39976f, 0.91662f, 0.39976f}, + {-0.89115f, -0.45372f, 0.89115f, 0.45372f}, + {-0.85466f, -0.51919f, 0.85466f, 0.51919f}, + {-0.80234f, -0.59687f, 0.80234f, 0.59687f}, + {-0.72909f, -0.68442f, 0.72909f, 0.68442f}, + {-0.63267f, -0.77442f, 0.63267f, 0.77442f}, + {-0.51899f, -0.85478f, 0.51899f, 0.85478f}, + {-0.40261f, -0.91537f, 0.40261f, 0.91537f}, + {-0.29860f, -0.95438f, 0.29860f, 0.95438f}, + {-0.21498f, -0.97662f, 0.21498f, 0.97662f}, + {-0.15215f, -0.98836f, 0.15215f, 0.98836f}, + {-0.10673f, -0.99429f, 0.10673f, 0.99429f}, + {-0.07452f, -0.99722f, 0.07452f, 0.99722f}, + {-0.05192f, -0.99865f, 0.05192f, 0.99865f}, + {-0.03612f, -0.99935f, 0.03612f, 0.99935f}, + {-0.02513f, -0.99968f, 0.02513f, 0.99968f}, + {-0.01747f, -0.99985f, 0.01747f, 0.99985f}, + {-0.01214f, -0.99993f, 0.01214f, 0.99993f}, }; const float quadBlockNy[20][4] PROGMEM = { - { -0.31891f, 0.94779f, 0.31891f, -0.94779f }, - { -0.35539f, 0.93472f, 0.35539f, -0.93472f }, - { -0.39976f, 0.91662f, 0.39976f, -0.91662f }, - { -0.45372f, 0.89115f, 0.45372f, -0.89115f }, - { -0.51919f, 0.85466f, 0.51919f, -0.85466f }, - { -0.59687f, 0.80234f, 0.59687f, -0.80234f }, - { -0.68442f, 0.72909f, 0.68442f, -0.72909f }, - { -0.77442f, 0.63267f, 0.77442f, -0.63267f }, - { -0.85478f, 0.51899f, 0.85478f, -0.51899f }, - { -0.91537f, 0.40261f, 0.91537f, -0.40261f }, - { -0.95438f, 0.29860f, 0.95438f, -0.29860f }, - { -0.97662f, 0.21498f, 0.97662f, -0.21498f }, - { -0.98836f, 0.15215f, 0.98836f, -0.15215f }, - { -0.99429f, 0.10673f, 0.99429f, -0.10673f }, - { -0.99722f, 0.07452f, 0.99722f, -0.07452f }, - { -0.99865f, 0.05192f, 0.99865f, -0.05192f }, - { -0.99935f, 0.03612f, 0.99935f, -0.03612f }, - { -0.99968f, 0.02513f, 0.99968f, -0.02513f }, - { -0.99985f, 0.01747f, 0.99985f, -0.01747f }, - { -0.99993f, 0.01214f, 0.99993f, -0.01214f }, + {-0.31891f, 0.94779f, 0.31891f, -0.94779f}, + {-0.35539f, 0.93472f, 0.35539f, -0.93472f}, + {-0.39976f, 0.91662f, 0.39976f, -0.91662f}, + {-0.45372f, 0.89115f, 0.45372f, -0.89115f}, + {-0.51919f, 0.85466f, 0.51919f, -0.85466f}, + {-0.59687f, 0.80234f, 0.59687f, -0.80234f}, + {-0.68442f, 0.72909f, 0.68442f, -0.72909f}, + {-0.77442f, 0.63267f, 0.77442f, -0.63267f}, + {-0.85478f, 0.51899f, 0.85478f, -0.51899f}, + {-0.91537f, 0.40261f, 0.91537f, -0.40261f}, + {-0.95438f, 0.29860f, 0.95438f, -0.29860f}, + {-0.97662f, 0.21498f, 0.97662f, -0.21498f}, + {-0.98836f, 0.15215f, 0.98836f, -0.15215f}, + {-0.99429f, 0.10673f, 0.99429f, -0.10673f}, + {-0.99722f, 0.07452f, 0.99722f, -0.07452f}, + {-0.99865f, 0.05192f, 0.99865f, -0.05192f}, + {-0.99935f, 0.03612f, 0.99935f, -0.03612f}, + {-0.99968f, 0.02513f, 0.99968f, -0.02513f}, + {-0.99985f, 0.01747f, 0.99985f, -0.01747f}, + {-0.99993f, 0.01214f, 0.99993f, -0.01214f}, }; const float quadBlockC[20][4] PROGMEM = { - { 49.3731f, -92.9228f, -41.1731f, 100.9228f }, - { 52.9126f, -82.0215f, -44.7126f, 90.0215f }, - { 56.7515f, -70.4863f, -48.5515f, 78.4863f }, - { 60.8192f, -58.1314f, -52.6192f, 66.1314f }, - { 64.9598f, -44.6227f, -56.7598f, 52.6227f }, - { 68.8007f, -29.6371f, -60.6007f, 37.6371f }, - { 71.6789f, -12.9798f, -63.4789f, 20.9798f }, - { 72.7161f, 5.1281f, -64.5161f, 2.8719f }, - { 71.2620f, 23.7112f, -63.0620f, -15.7112f }, - { 67.5191f, 41.3967f, -59.3191f, -33.3967f }, - { 62.4933f, 57.2743f, -54.2933f, -49.2743f }, - { 57.2945f, 71.2057f, -49.0945f, -63.2057f }, - { 52.6178f, 83.5542f, -44.4178f, -75.5542f }, - { 48.7270f, 94.7806f, -40.5270f, -86.7806f }, - { 45.6295f, 105.2669f, -37.4295f, -97.2669f }, - { 43.2291f, 115.2686f, -35.0291f, -107.2686f }, - { 41.3977f, 124.9666f, -33.1977f, -116.9666f }, - { 40.0175f, 134.4587f, -31.8175f, -126.4587f }, - { 38.9847f, 143.8219f, -30.7847f, -135.8219f }, - { 38.2169f, 153.0966f, -30.0169f, -145.0966f }, + {49.3731f, -92.9228f, -41.1731f, 100.9228f}, + {52.9126f, -82.0215f, -44.7126f, 90.0215f}, + {56.7515f, -70.4863f, -48.5515f, 78.4863f}, + {60.8192f, -58.1314f, -52.6192f, 66.1314f}, + {64.9598f, -44.6227f, -56.7598f, 52.6227f}, + {68.8007f, -29.6371f, -60.6007f, 37.6371f}, + {71.6789f, -12.9798f, -63.4789f, 20.9798f}, + {72.7161f, 5.1281f, -64.5161f, 2.8719f}, + {71.2620f, 23.7112f, -63.0620f, -15.7112f}, + {67.5191f, 41.3967f, -59.3191f, -33.3967f}, + {62.4933f, 57.2743f, -54.2933f, -49.2743f}, + {57.2945f, 71.2057f, -49.0945f, -63.2057f}, + {52.6178f, 83.5542f, -44.4178f, -75.5542f}, + {48.7270f, 94.7806f, -40.5270f, -86.7806f}, + {45.6295f, 105.2669f, -37.4295f, -97.2669f}, + {43.2291f, 115.2686f, -35.0291f, -107.2686f}, + {41.3977f, 124.9666f, -33.1977f, -116.9666f}, + {40.0175f, 134.4587f, -31.8175f, -126.4587f}, + {38.9847f, 143.8219f, -30.7847f, -135.8219f}, + {38.2169f, 153.0966f, -30.0169f, -145.0966f}, }; -// Draw an individual solid rectangular block for 4-quadrant view with smooth subpixel anti-aliasing +// Draw an individual solid rectangular block for 4-quadrant view with smooth +// subpixel anti-aliasing inline void drawQuadBarBlock(uint8_t cell, uint8_t i, uint16_t color) { int16_t ox = cell4_x[cell]; int16_t oy = cell4_y[cell]; @@ -481,7 +542,7 @@ inline void drawQuadBarBlock(uint8_t cell, uint8_t i, uint16_t color) { for (uint8_t k = 0; k < 4; k++) { nx[k] = pgm_read_float(&quadBlockNx[i][k]); ny[k] = pgm_read_float(&quadBlockNy[i][k]); - c[k] = pgm_read_float(&quadBlockC[i][k]); + c[k] = pgm_read_float(&quadBlockC[i][k]); } tft.startWrite(); @@ -493,9 +554,11 @@ inline void drawQuadBarBlock(uint8_t cell, uint8_t i, uint16_t color) { float min_d = 999.0f; for (uint8_t k = 0; k < 4; k++) { float d = px * nx[k] + py * ny[k] + c[k]; - if (d < min_d) min_d = d; + if (d < min_d) + min_d = d; } - if (min_d <= -0.5f) continue; + if (min_d <= -0.5f) + continue; int16_t sx = ox + x; if (color == TFT_BLACK) { tft.drawPixel(sx, sy, TFT_BLACK); @@ -772,7 +835,7 @@ void plotVBar(uint8_t cell, float data, int warn, int min, int max, } tft.setTextPadding(w - 6); tft.drawRightString(result.c_str(), x + w - 4, - screenHeight - (screenHeight * 0.12) + 1, 4); + screenHeight - (screenHeight * 0.12) + 1, 4); tft.setTextPadding(0); old_data[pidIndex] = data; // update old data } // if @@ -847,7 +910,8 @@ int get7SegWidth(const char *str, uint8_t dW, uint8_t t) { return total; } -// Draw blue header background for a 160x120 quadrant (25px height) with lateral and vertical smooth fade +// Draw blue header background for a 160x120 quadrant (25px height) with lateral +// and vertical smooth fade void drawQuadHeaderGradient(uint8_t cell) { int16_t ox = cell4_x[cell]; int16_t oy = cell4_y[cell]; @@ -873,7 +937,8 @@ void drawQuadHeaderGradient(uint8_t cell) { hx = t * t * (3.0f - 2.0f * t); } else if (x > (int16_t)x_right_start) { float t = (159.0f - (float)x) / fade_w_right; - if (t < 0.0f) t = 0.0f; + if (t < 0.0f) + t = 0.0f; hx = t * t * (3.0f - 2.0f * t); } float b = vy * hx; @@ -905,55 +970,183 @@ inline void drawQuadrantDividers() { // Determine display color for meter: // Instrumentos: Color del tema activo (normal) o TFT_RED (alarma/peligro) inline uint16_t getMeterColor(uint8_t pidIdx, float data, int warn = -1) { - if (pidIdx >= 4) return getThemePrimaryColor(); + if (pidIdx >= 4) + return getThemePrimaryColor(); float warnVal = (warn > 0) ? (float)warn : warningValue[pidIdx].toFloat(); if (pidIdx == 0) { // 0 = Temperatura ECT (PID 0105) - if (warnVal <= 0 || warnVal == 106.0f) warnVal = 110.0f; - if (data >= warnVal) return TFT_RED; + if (warnVal <= 0 || warnVal == 106.0f) + warnVal = 110.0f; + if (data >= warnVal) + return TFT_RED; return getThemePrimaryColor(); } if (pidIdx == 1) { // 1 = RPM (PID 010C) - if (warnVal <= 0) warnVal = 5000.0f; - if (data >= warnVal) return TFT_RED; + if (warnVal <= 0) + warnVal = 5000.0f; + if (data >= warnVal) + return TFT_RED; return getThemePrimaryColor(); } if (pidIdx == 2) { // 2 = Voltaje (PID 0142) - if (warnVal <= 0) warnVal = 15.0f; - // Si el dato es 0 (contacto apagado / desconectado) o motor detenido, no disparar alarma por < 12V - if (data <= 1.0f) return getThemePrimaryColor(); + if (warnVal <= 0) + warnVal = 15.0f; + // Si el dato es 0 (contacto apagado / desconectado) o motor detenido, no + // disparar alarma por < 12V + if (data <= 1.0f) + return getThemePrimaryColor(); if (currentRpm <= 0.0f) { - if (data >= warnVal) return TFT_RED; + if (data >= warnVal) + return TFT_RED; return getThemePrimaryColor(); } - if (data < 12.0f || data >= warnVal) return TFT_RED; + if (data < 12.0f || data >= warnVal) + return TFT_RED; return getThemePrimaryColor(); } if (pidIdx == 3) { // 3 = Velocidad (PID 010D) - if (warnVal <= 0) warnVal = 130.0f; - if (data >= warnVal) return TFT_RED; + if (warnVal <= 0) + warnVal = 130.0f; + if (data >= warnVal) + return TFT_RED; return getThemePrimaryColor(); } - if (warnVal > 0 && data >= warnVal) return TFT_RED; + if (warnVal > 0 && data >= warnVal) + return TFT_RED; return getThemePrimaryColor(); } -// Compute block count (0..NUM_BAR_BLOCKS) with sub-integer floating point precision +// Compute block count (0..NUM_BAR_BLOCKS) with sub-integer floating point +// precision inline int calcMeterBlocks(float val, int min, int max) { float range = (float)(max - min); - if (range <= 0.0f) return 0; - if (val <= (float)min) return 0; - if (val >= (float)max) return NUM_BAR_BLOCKS; + if (range <= 0.0f) + return 0; + if (val <= (float)min) + return 0; + if (val >= (float)max) + return NUM_BAR_BLOCKS; int b = (int)(((val - (float)min) / range) * (float)NUM_BAR_BLOCKS + 0.5f); - if (b < 0) b = 0; - if (b > NUM_BAR_BLOCKS) b = NUM_BAR_BLOCKS; + if (b < 0) + b = 0; + if (b > NUM_BAR_BLOCKS) + b = NUM_BAR_BLOCKS; return b; } +// Restore natural vignette vertical gradient where GPS icon sits +inline void clearPage0GpsIcon() { + uint16_t c1 = tft.alphaBlend((uint8_t)(255 * (82 - 3) / 82), + getThemeBgColor(), TFT_BLACK); + uint16_t c2 = tft.alphaBlend((uint8_t)(255 * (82 - 25) / 82), + getThemeBgColor(), TFT_BLACK); + tft.fillRectVGradient(294, 3, 24, 22, c1, c2); +} +inline void clearFullscreenGpsIcon() { + uint16_t c1 = tft.alphaBlend((uint8_t)(255 * (145 - 3) / 145), + getThemeBgColor(), TFT_BLACK); + uint16_t c2 = tft.alphaBlend((uint8_t)(255 * (145 - 25) / 145), + getThemeBgColor(), TFT_BLACK); + tft.fillRectVGradient(294, 3, 24, 22, c1, c2); +} -// 4-quadrant Exponential Segmented Block Meter (160x120 px) - identical to full screen style +// Draw animated GPS satellite icon in top right of Quadrant 1 (Velocidad: cell +// 1) +inline void drawPage0GpsIcon(uint8_t waveState, bool synced) { + if (currentScreenPage != 0 || fullScreenCell != -1) + return; + clearPage0GpsIcon(); + uint16_t waveCol = getThemePrimaryColor(); + uint16_t bodyCol = TFT_WHITE; + uint16_t bgCol = tft.alphaBlend((uint8_t)(255 * (82 - 3) / 82), + getThemeBgColor(), TFT_BLACK); + drawGpsSatelliteIcon(tft, 296, 3, bodyCol, waveCol, bgCol, waveState); +} + +// Draw animated GPS satellite icon in top right of Fullscreen Velocidad (cell +// 1) +inline void drawFullscreenGpsIcon(uint8_t waveState, bool synced) { + if (fullScreenCell != 1) + return; + clearFullscreenGpsIcon(); + uint16_t waveCol = getThemePrimaryColor(); + uint16_t bodyCol = TFT_WHITE; + uint16_t bgCol = tft.alphaBlend((uint8_t)(255 * (145 - 5) / 145), + getThemeBgColor(), TFT_BLACK); + drawGpsSatelliteIcon(tft, 296, 5, bodyCol, waveCol, bgCol, waveState); +} + +static uint8_t lastGpsIconWaveState = 255; +static bool lastGpsIconSyncState = false; +static bool lastGpsIconSpeed = false; +static int8_t lastGpsIconPage = -1; +static int8_t lastGpsIconFsCell = -2; + +inline void syncGpsIconAnimTrackers(int8_t page, int8_t fsCell) { + lastGpsIconWaveState = getGpsWaveAnimState(); + lastGpsIconSyncState = isGpsSynced(); + lastGpsIconSpeed = isGpsSpeedValid(); + lastGpsIconPage = page; + lastGpsIconFsCell = fsCell; +} + +// Live update of satellite icon animation in active screen +inline void updateGpsIconAnim() { + bool curGpsSpeed = isGpsSpeedValid(); + int8_t curPage = currentScreenPage; + int8_t curFsCell = fullScreenCell; + + // Si el GPS no esta activo entregando velocidad, no hay icono en pantalla + if (!curGpsSpeed) { + if (lastGpsIconSpeed) { + // Si antes estaba visible y se desconecto/perdio fix, restaurar el + // gradiente de fondo + if (curPage == 0 && curFsCell == -1) { + clearPage0GpsIcon(); + } else if (curFsCell == 1) { + clearFullscreenGpsIcon(); + } + lastGpsIconSpeed = false; + } + return; + } + + uint8_t curWaveState = getGpsWaveAnimState(); + bool curSync = isGpsSynced(); + + if (curWaveState == lastGpsIconWaveState && curSync == lastGpsIconSyncState && + curGpsSpeed == lastGpsIconSpeed && curPage == lastGpsIconPage && + curFsCell == lastGpsIconFsCell) { + return; + } + lastGpsIconWaveState = curWaveState; + lastGpsIconSyncState = curSync; + lastGpsIconSpeed = curGpsSpeed; + lastGpsIconPage = curPage; + lastGpsIconFsCell = curFsCell; + + if (curPage == 0 && curFsCell == -1) { + drawPage0GpsIcon(curWaveState, curSync); + } else if (curFsCell == 1) { + drawFullscreenGpsIcon(curWaveState, curSync); + } +} + +// Synchronize Page 0 tracking variables without touching TFT display (zero +// flicker) +inline void syncPage0Trackers() { + badgeEcoDrawn = isEcoOptimal; + for (uint8_t c = 0; c < 4; c++) { + old_data[c] = last_valid_data[c]; + wasInRedAlarm[c] = false; + } + syncGpsIconAnimTrackers(0, -1); +} + +// 4-quadrant Exponential Segmented Block Meter (160x120 px) - identical to full +// screen style void arcMeter4(uint8_t cell, uint8_t pid, bool redrawHeader = true) { int x = cell4_x[cell]; int y = cell4_y[cell]; @@ -990,6 +1183,9 @@ void arcMeter4(uint8_t cell, uint8_t pid, bool redrawHeader = true) { tft.unloadFont(); old_data[cell] = -999; } + if (cell == 1 && isGpsSpeedValid()) { + drawPage0GpsIcon(getGpsWaveAnimState(), isGpsSynced()); + } } void plotArc4(uint8_t cell, float data, int warn, int min, int max, bool digit, @@ -1001,12 +1197,15 @@ void plotArc4(uint8_t cell, float data, int warn, int min, int max, bool digit, uint8_t pidIdx = pidInCell[cell]; uint16_t col = getMeterColor(pidIdx, data, warn); - uint16_t oldCol = (old_data[cell] <= -900) ? 0 : getMeterColor(pidIdx, old_data[cell], warn); + uint16_t oldCol = (old_data[cell] <= -900) + ? 0 + : getMeterColor(pidIdx, old_data[cell], warn); String numStr = String(data, digit); int newBlocks = calcMeterBlocks(data, min, max); - int oldBlocks = (old_data[cell] <= -900) ? 0 : calcMeterBlocks(old_data[cell], min, max); + int oldBlocks = + (old_data[cell] <= -900) ? 0 : calcMeterBlocks(old_data[cell], min, max); bool colorChanged = (col != oldCol); if (col == TFT_RED) { @@ -1067,6 +1266,12 @@ void plotArc4(uint8_t cell, float data, int warn, int min, int max, bool digit, tft.setTextPadding(0); tft.unloadFont(); old_data[cell] = data; + + // Draw GPS Satellite Icon & indicator in top-right corner of cell 1 + // (Velocidad) + if (cell == 1 && isGpsSpeedValid()) { + drawPage0GpsIcon(getGpsWaveAnimState(), isGpsSynced()); + } } void fullScreenMeter(uint8_t cell, uint8_t pid, bool redrawHeader = true) { @@ -1077,16 +1282,26 @@ void fullScreenMeter(uint8_t cell, uint8_t pid, bool redrawHeader = true) { unit = "V"; if (redrawHeader) { - // Ambient vignette gradient from y=0 fading to black (matching 4 quadrants style) + // Ambient vignette gradient from y=0 fading to black (matching 4 quadrants + // style) tft.fillRectVGradient(0, 0, 320, 145, getThemeBgColor(), TFT_BLACK); tft.fillRect(0, 145, 320, 95, TFT_BLACK); drawPidIconHeader(10, 4, pidConfig[pid][2]); - uint16_t titleCol = (getMeterColor(pid, last_valid_data[cell]) == TFT_RED && alarmEnabled[pid]) ? TFT_RED : TFT_WHITE; + uint16_t titleCol = (getMeterColor(pid, last_valid_data[cell]) == TFT_RED && + alarmEnabled[pid]) + ? TFT_RED + : TFT_WHITE; tft.setTextColor(titleCol); - tft.drawCentreString(label.c_str(), 175, 12, 4); + tft.drawCentreString(label, 175, 12, 4); + if (cell == 1 && isGpsSpeedValid()) { + drawFullscreenGpsIcon(getGpsWaveAnimState(), isGpsSynced()); + } } else { - tft.fillRectVGradient(0, 51, 320, 94, tft.alphaBlend((uint8_t)(255 * (145 - 51) / 145), getThemeBgColor(), TFT_BLACK), TFT_BLACK); + tft.fillRectVGradient(0, 51, 320, 94, + tft.alphaBlend((uint8_t)(255 * (145 - 51) / 145), + getThemeBgColor(), TFT_BLACK), + TFT_BLACK); tft.fillRect(0, 145, 320, 95, TFT_BLACK); } @@ -1099,7 +1314,8 @@ void fullScreenMeter(uint8_t cell, uint8_t pid, bool redrawHeader = true) { old_data[cell] = -999; plotFullScreen(cell, last_valid_data[cell], warn, min, max, digit, unit); } else { - // Exponential segmented block mode full screen (18 discrete blocks, 3mm x 5mm, 1mm gap) + // Exponential segmented block mode full screen (18 discrete blocks, 3mm x + // 5mm, 1mm gap) for (uint8_t i = 0; i < NUM_BAR_BLOCKS; i++) { drawBarBlock(i, METER_INACTIVE_COLOR); } @@ -1120,12 +1336,15 @@ void plotFullScreen(uint8_t cell, float data, int warn, int min, int max, return; uint8_t pidIdx = pidInCell[cell]; uint16_t col = getMeterColor(pidIdx, data, warn); - uint16_t oldCol = (old_data[cell] <= -900) ? 0 : getMeterColor(pidIdx, old_data[cell], warn); + uint16_t oldCol = (old_data[cell] <= -900) + ? 0 + : getMeterColor(pidIdx, old_data[cell], warn); String numStr = String(data, digit); int newBlocks = calcMeterBlocks(data, min, max); - int oldBlocks = (old_data[cell] <= -900) ? 0 : calcMeterBlocks(old_data[cell], min, max); + int oldBlocks = + (old_data[cell] <= -900) ? 0 : calcMeterBlocks(old_data[cell], min, max); bool colorChanged = (col != oldCol); if (col == TFT_RED) { @@ -1190,11 +1409,21 @@ void plotFullScreen(uint8_t cell, float data, int warn, int min, int max, tft.setTextPadding(0); tft.unloadFont(); old_data[cell] = data; + + if (cell == 1 && isGpsSpeedValid()) { + drawFullscreenGpsIcon(getGpsWaveAnimState(), isGpsSynced()); + } } -// Display "---" (no data) with all blocks inactive on specified quadrant or full screen +// Display "---" (no data) with all blocks inactive on specified quadrant or +// full screen void plotNoData(uint8_t cell) { - if (cell >= 4) return; + if (cell >= 4) + return; + if (cell == 1 && isGpsSpeedValid()) { + return; // Si el GPS esta activo y entregando velocidad, nunca pisar con + // "---" + } if (old_data[cell] <= -900.0f && last_valid_data[cell] <= -900.0f) { return; // Already showing no data, skip redraw to avoid SPI overhead } @@ -1236,71 +1465,99 @@ void plotNoData(uint8_t cell) { } } -// Draw parameter icon into sprite slice with true alpha channel -inline void drawPidIconToSprite(TFT_eSprite &s, int ix, int iy, const String &pidStr, int slice_h) { +// Draw parameter icon into sprite slice with true 8-bit alpha channel blended +// against theme background +inline void drawPidIconToSprite(TFT_eSprite &s, int ix, int iy, + const String &pidStr, int y_slice, int slice_h, + int global_ry) { int w = 42, h = 42; const unsigned short *colors = nullptr; const unsigned char *alphas = nullptr; if (pidStr == "0105") { - w = 38; h = 37; - colors = icon_temp_quad; alphas = icon_temp_quad_alpha; + w = 38; + h = 37; + colors = icon_temp_quad; + alphas = icon_temp_quad_alpha; } else if (pidStr == "010D") { - w = 42; h = 42; - colors = icon_speed_quad; alphas = icon_speed_quad_alpha; + w = 42; + h = 42; + colors = icon_speed_quad; + alphas = icon_speed_quad_alpha; } else if (pidStr == "010C") { - w = 42; h = 43; - colors = icon_rpm_quad; alphas = icon_rpm_quad_alpha; + w = 42; + h = 43; + colors = icon_rpm_quad; + alphas = icon_rpm_quad_alpha; } else if (pidStr == "0142" || pidStr == "ATRV") { - w = 46; h = 35; - colors = icon_battery_quad; alphas = icon_battery_quad_alpha; + w = 46; + h = 35; + colors = icon_battery_quad; + alphas = icon_battery_quad_alpha; } - if (!colors || !alphas) return; + if (!colors || !alphas) + return; int startJ = (iy < 0) ? -iy : 0; int endJ = (iy + h > slice_h) ? (slice_h - iy) : h; - if (startJ >= endJ) return; + if (startJ >= endJ) + return; for (int j = startJ; j < endJ; j++) { int curY = iy + j; + int globalY = y_slice + curY; + uint16_t bgCol = getInstrumentBgColor(globalY, true, global_ry); int rowOffset = j * w; for (int i = 0; i < w; i++) { int curX = ix + i; - if (curX < 0 || curX >= 320) continue; + if (curX < 0 || curX >= 320) + continue; uint8_t a = pgm_read_byte(&alphas[rowOffset + i]); - if (a >= 30) { - uint16_t fg = pgm_read_word(&colors[rowOffset + i]); + if (a == 0) + continue; + uint16_t fg = pgm_read_word(&colors[rowOffset + i]); + if (a >= 250) { s.drawPixel(curX, curY, fg); + } else { + s.drawPixel(curX, curY, fastBlend(a, fg, bgCol)); } } } } -// Draw an individual solid curved block into sprite slice with subpixel anti-aliasing -inline void drawQuadBarBlockToSprite(TFT_eSprite &s, int16_t qx, int16_t qy, uint8_t i, uint16_t color, int slice_h) { +// Draw an individual solid curved block into sprite slice with subpixel +// anti-aliasing +inline void drawQuadBarBlockToSprite(TFT_eSprite &s, int16_t qx, int16_t qy, + uint8_t i, uint16_t color, int slice_h) { int16_t minX = pgm_read_word(&quadBlockMinX[i]); int16_t maxX = pgm_read_word(&quadBlockMaxX[i]); int16_t minY = pgm_read_word(&quadBlockMinY[i]); int16_t maxY = pgm_read_word(&quadBlockMaxY[i]); - if (qx + maxX < 0 || qx + minX >= 320) return; - if (qy + maxY < 0 || qy + minY >= slice_h) return; + if (qx + maxX < 0 || qx + minX >= 320) + return; + if (qy + maxY < 0 || qy + minY >= slice_h) + return; int16_t startY = minY; int16_t endY = maxY; - if (qy + startY < 0) startY = -qy; - if (qy + endY >= slice_h) endY = slice_h - 1 - qy; + if (qy + startY < 0) + startY = -qy; + if (qy + endY >= slice_h) + endY = slice_h - 1 - qy; int16_t startX = minX; int16_t endX = maxX; - if (qx + startX < 0) startX = -qx; - if (qx + endX >= 320) endX = 319 - qx; + if (qx + startX < 0) + startX = -qx; + if (qx + endX >= 320) + endX = 319 - qx; float nx[4], ny[4], c[4]; for (uint8_t k = 0; k < 4; k++) { nx[k] = pgm_read_float(&quadBlockNx[i][k]); ny[k] = pgm_read_float(&quadBlockNy[i][k]); - c[k] = pgm_read_float(&quadBlockC[i][k]); + c[k] = pgm_read_float(&quadBlockC[i][k]); } for (int16_t y = startY; y <= endY; y++) { @@ -1311,9 +1568,11 @@ inline void drawQuadBarBlockToSprite(TFT_eSprite &s, int16_t qx, int16_t qy, uin float min_d = 999.0f; for (uint8_t k = 0; k < 4; k++) { float d = px * nx[k] + py * ny[k] + c[k]; - if (d < min_d) min_d = d; + if (d < min_d) + min_d = d; } - if (min_d <= -0.5f) continue; + if (min_d <= -0.5f) + continue; int16_t sx = qx + x; if (color == TFT_BLACK) { s.drawPixel(sx, sy, TFT_BLACK); @@ -1330,7 +1589,8 @@ inline void drawQuadBarBlockToSprite(TFT_eSprite &s, int16_t qx, int16_t qy, uin } // Render Page 0 (4 Quadrants) into double-buffered Sprite slice -inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) { +inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, + int slice_h) { checkUpdateThemeGradients(); uint16_t divCol = getThemeDivColor(); uint16_t priCol = getThemePrimaryColor(); @@ -1339,9 +1599,11 @@ inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s int qx = cell4_x[c] + x_offset; int qy = cell4_y[c] - y_slice; int rx = (c & 1) ? (161 + x_offset) : (0 + x_offset); - int ry = ((c & 2) ? 121 : 0) - y_slice; + int global_ry = (c & 2) ? 121 : 0; + int ry = global_ry - y_slice; - if (qx + 160 <= 0 || qx >= 320 || ry + 119 <= 0 || ry >= slice_h) continue; + if (qx + 160 <= 0 || qx >= 320 || ry + 119 <= 0 || ry >= slice_h) + continue; // 1. Exact dark vignette gradient matching real arcMeter4 s.fillRect(rx, ry, 159, 119, TFT_BLACK); @@ -1356,10 +1618,13 @@ inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s uint8_t pidIdx = pidInCell[c]; - // 2. Real high-definition parameter icon (thermometer, speedometer, tachometer, battery) - drawPidIconToSprite(s, qx + 5, qy + 3, pidConfig[pidIdx][2], slice_h); + // 2. Real high-definition parameter icon (thermometer, speedometer, + // tachometer, battery) + drawPidIconToSprite(s, qx + 5, qy + 3, pidConfig[pidIdx][2], y_slice, + slice_h, global_ry); - // 3. Exact 20 curved segmented gauge blocks along the arc (true mathematical polygons) + // 3. Exact 20 curved segmented gauge blocks along the arc (true + // mathematical polygons) int warn = warningValue[pidIdx].toInt(); int min = pidConfig[pidIdx][4].toInt(); int max = pidConfig[pidIdx][5].toInt(); @@ -1367,14 +1632,16 @@ inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s float val = last_valid_data[c]; int numActive = (val > -900.0f) ? calcMeterBlocks(val, min, max) : 0; - uint16_t activeCol = (val > -900.0f) ? getMeterColor(pidIdx, val, warn) : priCol; + uint16_t activeCol = + (val > -900.0f) ? getMeterColor(pidIdx, val, warn) : priCol; for (uint8_t i = 0; i < NUM_BAR_BLOCKS; i++) { uint16_t bCol = (i < numActive) ? activeCol : METER_INACTIVE_COLOR; drawQuadBarBlockToSprite(s, qx, qy, i, bCol, slice_h); } - // 4. Digital value string using authentic DSEG7 34pt font (pixel-perfect match with resting screen) + // 4. Digital value string using authentic DSEG7 34pt font (pixel-perfect + // match with resting screen) String numStr = (val <= -900.0f) ? "---" : String(val, digit); int textY = qy + 74; if (textY < slice_h && textY + 40 > 0) { @@ -1384,6 +1651,15 @@ inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s s.drawRightString(numStr.c_str(), qx + 152, textY, 1); s.fontLoaded = false; } + + // GPS Satellite icon in top right of Quadrant 1 (Velocidad: cell 1) + if (c == 1 && isGpsSpeedValid() && y_slice == 0) { + uint16_t waveCol = priCol; + uint16_t bodyCol = TFT_WHITE; + uint16_t bgCol = themeGradQuad[3]; + drawGpsSatelliteIcon(s, qx + 136, 3, bodyCol, waveCol, bgCol, + getGpsWaveAnimState()); + } } // 5. Eco Badge if active @@ -1415,14 +1691,17 @@ inline void renderPage0ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s } // Render Page 1 (3 Temperature Cards) into double-buffered Sprite slice -inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int slice_h) { +inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, + int slice_h) { for (uint8_t cardIdx = 0; cardIdx < 3; cardIdx++) { int cardY = (6 + cardIdx * 77) - y_slice; int cardH = 73; int cardX = 4 + x_offset; int cardW = 312; - if (cardX + cardW <= 0 || cardX >= 320 || cardY + cardH <= 0 || cardY >= slice_h) continue; + if (cardX + cardW <= 0 || cardX >= 320 || cardY + cardH <= 0 || + cardY >= slice_h) + continue; uint16_t cardBg = 0x10A2; uint16_t cardBorder = 0x2945; @@ -1431,38 +1710,55 @@ inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s s.drawRoundRect(cardX, cardY, cardW, cardH, 6, cardBorder); uint16_t accentCol = getThemePrimaryColor(); - const char* titleStr = ""; - const char* subStr = ""; + const char *titleStr = ""; + const char *subStr = ""; float val = 0.0f; float minRange = 0.0f, maxRange = 100.0f; bool isInt = false; if (cardIdx == 0) { - titleStr = "MOTOR / REFRIGERANTE"; + titleStr = "AGUA"; subStr = ignitionOn ? "ECU OBD-II (PID 0105)" : "ECU OBD-II (STANDBY)"; val = tempEngineCoolant; - minRange = 60.0f; maxRange = 120.0f; isInt = true; - if (val >= 110.0f) accentCol = TFT_RED; - else if (val >= 100.0f) accentCol = TFT_YELLOW; + minRange = 60.0f; + maxRange = 120.0f; + isInt = true; + if (val >= 110.0f) + accentCol = TFT_RED; + else if (val >= 100.0f) + accentCol = TFT_YELLOW; } else if (cardIdx == 1) { - titleStr = "HABITACULO / INTERIOR"; - subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1" : "DS18B20 SENSOR 1 (DEMO)"; + titleStr = "INTERIOR"; + subStr = (dsSensorCount > 0) ? "DS18B20 SENSOR 1 (GPIO 22)" + : "DS18B20 SENSOR 1"; val = tempInterior; - minRange = 10.0f; maxRange = 40.0f; + minRange = 10.0f; + maxRange = 40.0f; accentCol = 0x07F3; } else { - titleStr = "AMBIENTE / EXTERIOR"; - subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2" : "DS18B20 SENSOR 2 (DEMO)"; + titleStr = "EXTERIOR"; + subStr = (dsSensorCount > 1) ? "DS18B20 SENSOR 2 (GPIO 22)" + : (dsSensorCount == 1 ? "ESTIMADA (-4.5`C)" + : "DS18B20 SENSOR 2"); val = tempExterior; - minRange = -10.0f; maxRange = 50.0f; + minRange = -10.0f; + maxRange = 50.0f; accentCol = 0x3D7F; } s.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol); - s.setTextColor(TFT_WHITE, cardBg); - s.drawString(titleStr, cardX + 16, cardY + 8, 2); + // Smooth anti-aliased title in transition sprite (zero mallocs, 100% + // smooth) + if (!sprTriTitleReady) + initTriTempTitles(); + if (sprTriTitleReady && sprTriTitle[cardIdx].created()) { + sprTriTitle[cardIdx].pushToSprite(&s, cardX + 16, cardY + 6); + } else { + s.setTextColor(TFT_WHITE, cardBg); + s.drawString(titleStr, cardX + 16, cardY + 6, 4); + } s.setTextColor(0x8410, cardBg); - s.drawString(subStr, cardX + 16, cardY + 28, 1); + s.drawString(subStr, cardX + 16, cardY + 34, 1); // Progress bar int barX = cardX + 16; @@ -1470,163 +1766,393 @@ inline void renderPage1ToSprite(TFT_eSprite &s, int x_offset, int y_slice, int s int barW = 120; s.fillRoundRect(barX, barY, barW, 8, 3, 0x18C3); float ratio = (val - minRange) / (maxRange - minRange); - if (ratio < 0.0f) ratio = 0.0f; - if (ratio > 1.0f) ratio = 1.0f; + 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 < 4) + fillW = 4; s.fillRoundRect(barX, barY, fillW, 8, 3, accentCol); // Value text using authentic DSEG7 34pt font matching drawTriTempCard - String numStr = (val <= -900.0f) ? "---" : (isInt ? String((int)val) : String(val, 1)); - int numY = cardY + 22; + String numStr = + (val <= -900.0f) ? "---" : (isInt ? String((int)val) : String(val, 1)); + int numY = cardY + 20; if (numY < slice_h && numY + 40 > 0) { s.fontLoaded = true; s.setTextColor(accentCol, cardBg); s.setTextPadding(0); - s.drawRightString(numStr.c_str(), cardX + 262, numY, 1); + s.drawRightString(numStr.c_str(), cardX + 270, numY, 1); s.fontLoaded = false; + // Unit "`C" during transition animation aligned to digit base + s.setTextColor(accentCol, cardBg); + s.drawString("`C", cardX + 276, cardY + 37, 2); } - - s.setTextColor(0xC618, cardBg); - s.drawString("`C", cardX + 268, cardY + 32, 2); } } // 100% Flicker-Free Double-Buffered Mobile Slide Transition // Safe 25KB sprite with pixel-perfect clipping and smooth ease-out pacing -inline void animateSlideTransition(bool swipeLeft) { - Serial.println(">>> [DB] animateSlideTransition ENTER"); - const int slice_h = 40; // 320x40 = only 25KB (100% safe, leaves 70KB+ free for Bluetooth) +inline void animateSlideTransition(bool swipeLeft, int8_t fromPage = -1, + int8_t toPage = -1) { + if (fromPage < 0) + fromPage = currentScreenPage; + if (toPage < 0) + toPage = (fromPage == 0) ? 1 : 0; +#ifdef SERIAL_DEBUG + Serial.printf(">>> [DB] animateSlideTransition ENTER: fromPage=%d, " + "toPage=%d, swipeLeft=%d\n", + fromPage, toPage, swipeLeft); +#endif + const int slice_h = 40; const uint8_t numSlices = 240 / slice_h; // exactly 6 slices - TFT_eSprite spr(&tft); - if (spr.createSprite(320, slice_h) == nullptr) { - Serial.println(">>> [DB] spr.createSprite FAILED (nullptr)!"); + if (!sprTransAllocated) { + initScreen(true); return; // Safety fallback } - spr.setTextFont(1); // CRITICAL: initializes textfont=1 and clears uninitialized gfxFont=NULL - spr.setFreeFont(NULL); // CRITICAL: prevents crash in textWidth() when drawing Font 1 on stack sprite - spr.loadFont(DSEG7_Modern_BoldItalic_34); - spr.fontLoaded = false; // Default to standard fonts until DSEG7 digits are drawn - Serial.printf(">>> [DB] Sprite created OK with DSEG7. Heap: %u, MaxAlloc: %u\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); const uint8_t FRAMES = 28; // High-density sine ease-out deceleration over 28 frames: - // Ultra-fine 10-18px initial steps smoothly tapering to 1-3px for zero visible refresh chop + // Ultra-fine 10-18px initial steps smoothly tapering to 1-3px for zero + // visible refresh chop const int16_t step_shifts[FRAMES] = { - 18, 36, 54, 71, 89, 106, 122, 139, 155, 170, - 185, 200, 213, 226, 239, 250, 261, 271, 280, 288, - 296, 302, 307, 312, 315, 318, 319, 320 - }; + 18, 36, 54, 71, 89, 106, 122, 139, 155, 170, 185, 200, 213, 226, + 239, 250, 261, 271, 280, 288, 296, 302, 307, 312, 315, 318, 319, 320}; for (uint8_t f = 0; f < FRAMES; f++) { - if (f == 0 || f == FRAMES / 2 || f == FRAMES - 1) { - Serial.printf(">>> [DB] Frame %d / %d (heap: %u)\n", f, FRAMES, ESP.getFreeHeap()); - } int16_t shift = step_shifts[f]; int16_t splitX = swipeLeft ? (320 - shift) : shift; - int16_t x0 = swipeLeft ? (-shift) : (shift - 320); - int16_t x1 = swipeLeft ? (320 - shift) : shift; for (uint8_t s = 0; s < numSlices; s++) { int y_slice = s * slice_h; - spr.fillSprite(TFT_BLACK); + sprTrans.fillSprite(TFT_BLACK); - if (splitX > 0) { - spr.setViewport(0, 0, splitX, slice_h, false); - renderPage0ToSprite(spr, x0, y_slice, slice_h); + if (swipeLeft) { + // Swipe Left: fromPage on left moving left, toPage on right moving left + if (splitX > 0) { + sprTrans.setViewport(0, 0, splitX, slice_h, false); + if (fromPage == 0) + renderPage0ToSprite(sprTrans, -shift, y_slice, slice_h); + else if (fromPage == 1) + renderPage1ToSprite(sprTrans, -shift, y_slice, slice_h); + else if (fromPage == 2) + renderPage2ToSprite(sprTrans, -shift, y_slice, slice_h); + else + renderPage3ToSprite(sprTrans, -shift, y_slice, slice_h); + } + if (splitX < 320) { + sprTrans.setViewport(splitX, 0, 320 - splitX, slice_h, false); + if (toPage == 0) + renderPage0ToSprite(sprTrans, 320 - shift, y_slice, slice_h); + else if (toPage == 1) + renderPage1ToSprite(sprTrans, 320 - shift, y_slice, slice_h); + else if (toPage == 2) + renderPage2ToSprite(sprTrans, 320 - shift, y_slice, slice_h); + else + renderPage3ToSprite(sprTrans, 320 - shift, y_slice, slice_h); + } + } else { + // Swipe Right: toPage on left moving right, fromPage on right moving + // right + if (splitX > 0) { + sprTrans.setViewport(0, 0, splitX, slice_h, false); + if (toPage == 0) + renderPage0ToSprite(sprTrans, shift - 320, y_slice, slice_h); + else if (toPage == 1) + renderPage1ToSprite(sprTrans, shift - 320, y_slice, slice_h); + else if (toPage == 2) + renderPage2ToSprite(sprTrans, shift - 320, y_slice, slice_h); + else + renderPage3ToSprite(sprTrans, shift - 320, y_slice, slice_h); + } + if (splitX < 320) { + sprTrans.setViewport(splitX, 0, 320 - splitX, slice_h, false); + if (fromPage == 0) + renderPage0ToSprite(sprTrans, shift, y_slice, slice_h); + else if (fromPage == 1) + renderPage1ToSprite(sprTrans, shift, y_slice, slice_h); + else if (fromPage == 2) + renderPage2ToSprite(sprTrans, shift, y_slice, slice_h); + else + renderPage3ToSprite(sprTrans, shift, y_slice, slice_h); + } } - if (splitX < 320) { - spr.setViewport(splitX, 0, 320 - splitX, slice_h, false); - renderPage1ToSprite(spr, x1, y_slice, slice_h); - } - spr.resetViewport(); + sprTrans.resetViewport(); // Dividing neon seam line if (splitX > 0 && splitX < 320) { - spr.drawFastVLine(splitX, 0, slice_h, getThemePrimaryColor()); - spr.drawFastVLine(splitX + (swipeLeft ? 1 : -1), 0, slice_h, TFT_WHITE); + sprTrans.drawFastVLine(splitX, 0, slice_h, getThemePrimaryColor()); + int16_t whiteLineX = splitX + (swipeLeft ? 1 : -1); + if (whiteLineX >= 0 && whiteLineX < 320) { + sprTrans.drawFastVLine(whiteLineX, 0, slice_h, TFT_WHITE); + } } - // Pagination indicator dots on top slice + // Pagination indicator dots on top slice (4 dots for 4 pages) if (y_slice == 0) { - spr.fillCircle(153, 2, 2, (splitX > 160) ? getThemePrimaryColor() : 0x5AEB); - spr.fillCircle(167, 2, 2, (splitX <= 160) ? getThemePrimaryColor() : 0x5AEB); + uint8_t activeDot = (shift < 160) ? fromPage : toPage; + sprTrans.fillCircle(139, 2, 2, + (activeDot == 0) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(153, 2, 2, + (activeDot == 1) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(167, 2, 2, + (activeDot == 2) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(181, 2, 2, + (activeDot == 3) ? getThemePrimaryColor() : 0x5AEB); } // Direct single SPI push per slice: ZERO visual flicker! - spr.pushSprite(0, y_slice); + sprTrans.pushSprite(0, y_slice); + yield(); } yield(); // Feed watchdog and let Bluetooth run smoothly on background core delay(7); // Ultra-smooth high density pacing (~330ms total) } - spr.fontLoaded = true; - spr.unloadFont(); - spr.deleteSprite(); // Release the 25KB - Serial.printf(">>> [DB] animateSlideTransition LEAVE (heap: %u)\n", ESP.getFreeHeap()); + sprTrans.fontLoaded = false; +} + +// 100% Flicker-Free Double-Buffered Crossfade Transition +// Ultra-fast parallel 32-bit RGB565 blend with zero static RAM allocation +inline void animateFadeTransition(int8_t fromPage = -1, int8_t toPage = -1) { + if (fromPage < 0) + fromPage = currentScreenPage; + if (toPage < 0) + toPage = (fromPage == 0) ? 1 : 0; +#ifdef SERIAL_DEBUG + Serial.printf(">>> [DB] animateFadeTransition ENTER: fromPage=%d, toPage=%d, " + "heap=%u, maxAlloc=%u\n", + fromPage, toPage, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); +#endif + + const int slice_h = 40; + const uint8_t numSlices = 240 / slice_h; // exactly 6 slices + + if (!sprTransAllocated) { + initScreen(true); + return; + } + + TFT_eSprite sprB(&tft); + if (sprB.createSprite(320, slice_h) == nullptr) { + animateSlideTransition(true, fromPage, toPage); + return; + } + + sprB.setTextFont(1); + sprB.setFreeFont(NULL); + sprB.loadFont(DSEG7_Modern_BoldItalic_34); + sprB.fontLoaded = false; + + const uint8_t FRAMES = 10; + // Perceptual crossfade steps (0 to 32) + const uint8_t alpha_steps[FRAMES] = {3, 6, 10, 14, 18, 22, 26, 29, 31, 32}; + const int totalPixels = 320 * slice_h; + + for (uint8_t f = 0; f < FRAMES; f++) { + uint8_t alpha = alpha_steps[f]; + + for (uint8_t s = 0; s < numSlices; s++) { + int y_slice = s * slice_h; + + sprTrans.fillSprite(TFT_BLACK); + sprB.fillSprite(TFT_BLACK); + + if (fromPage == 0) + renderPage0ToSprite(sprTrans, 0, y_slice, slice_h); + else if (fromPage == 1) + renderPage1ToSprite(sprTrans, 0, y_slice, slice_h); + else if (fromPage == 2) + renderPage2ToSprite(sprTrans, 0, y_slice, slice_h); + else + renderPage3ToSprite(sprTrans, 0, y_slice, slice_h); + + if (toPage == 0) + renderPage0ToSprite(sprB, 0, y_slice, slice_h); + else if (toPage == 1) + renderPage1ToSprite(sprB, 0, y_slice, slice_h); + else if (toPage == 2) + renderPage2ToSprite(sprB, 0, y_slice, slice_h); + else + renderPage3ToSprite(sprB, 0, y_slice, slice_h); + + uint16_t *pA = (uint16_t *)sprTrans.getPointer(); + uint16_t *pB = (uint16_t *)sprB.getPointer(); + + if (pA && pB) { + for (int i = 0; i < totalPixels; i++) { + uint16_t rawA = pA[i]; + uint16_t rawB = pB[i]; + if (rawA == rawB) + continue; + + // Convert byte-swapped format from sprite to RGB565 + uint16_t c1 = (rawA >> 8) | (rawA << 8); + uint16_t c2 = (rawB >> 8) | (rawB << 8); + + // Fast parallel 32-bit blend of Red/Blue and Green + uint32_t rb1 = c1 & 0xF81F; + uint32_t g1 = c1 & 0x07E0; + uint32_t rb2 = c2 & 0xF81F; + uint32_t g2 = c2 & 0x07E0; + + uint32_t rb = ((rb1 * (32 - alpha) + rb2 * alpha) >> 5) & 0xF81F; + uint32_t g = ((g1 * (32 - alpha) + g2 * alpha) >> 5) & 0x07E0; + + uint16_t blended = (uint16_t)(rb | g); + pA[i] = (blended >> 8) | (blended << 8); + } + } + + // Pagination indicator dots on top slice (4 dots for 4 pages) + if (y_slice == 0) { + uint8_t activeDot = (alpha < 16) ? fromPage : toPage; + sprTrans.fillCircle(139, 2, 2, + (activeDot == 0) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(153, 2, 2, + (activeDot == 1) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(167, 2, 2, + (activeDot == 2) ? getThemePrimaryColor() : 0x5AEB); + sprTrans.fillCircle(181, 2, 2, + (activeDot == 3) ? getThemePrimaryColor() : 0x5AEB); + } + + sprTrans.pushSprite(0, y_slice); + yield(); + } + + yield(); + delay(10); + } + + sprB.fontLoaded = true; + sprB.unloadFont(); + sprB.deleteSprite(); + sprTrans.fontLoaded = false; +} + +// Master transition dispatcher based on user configuration +inline void performScreenTransition(bool swipeLeft, int8_t fromPage = -1, + int8_t toPage = -1) { + if (fromPage < 0) + fromPage = currentScreenPage; + if (toPage < 0) + toPage = (fromPage + 1) % 4; + if (transitionType == 1) { + animateFadeTransition(fromPage, toPage); + } else { + animateSlideTransition(swipeLeft, fromPage, toPage); + } } /*-------------------*/ // initialize display -void initScreen() { +void initScreen(bool redrawTFT) { terminalLineCount = 0; - Serial.printf(">>> [DB] initScreen START: page=%d, fullScreenCell=%d, heap=%u\n", currentScreenPage, fullScreenCell, ESP.getFreeHeap()); +#ifdef SERIAL_DEBUG + Serial.printf(">>> [DB] initScreen START: page=%d, fullScreenCell=%d, " + "redrawTFT=%d, heap=%u\n", + currentScreenPage, fullScreenCell, redrawTFT, + ESP.getFreeHeap()); +#endif for (int i = 0; i < 4; i++) { pidList[i] = pidConfig[pidInCell[i]][2]; // get PID to list pidReadSkip[i] = pidConfig[pidInCell[i]][6].toInt(); // get read skip pidCurrentSkip[i] = pidReadSkip[i]; // skip for skip reading pid - old_data[i] = -999; // clear old data } for (int i = 4; i < maxpidIndex; i++) { pidList[i] = pidConfig[i][2]; // get background PIDs (e.g. 0104) pidReadSkip[i] = pidConfig[i][6].toInt(); // get read skip pidCurrentSkip[i] = pidReadSkip[i]; + } + + if (!redrawTFT) { + // Screen is ALREADY 100% rendered with zero flicker by the double-buffered + // transition engine! ONLY sync tracking variables so live updates continue + // seamlessly without redrawing or blanking! + if (currentScreenPage == 1) { + syncTriTempTrackers(); + } else if (currentScreenPage == 2) { + syncGpsClockTrackers(); + } else if (currentScreenPage == 3) { + syncGpsNavTrackers(); + } else if (currentScreenPage == 0 && fullScreenCell == -1) { + syncPage0Trackers(); + } +#ifdef SERIAL_DEBUG + Serial.printf(">>> [DB] initScreen (NO REDRAW) FINISHED OK (heap: %u)\n", + ESP.getFreeHeap()); +#endif + return; + } + + for (int i = 0; i < maxpidIndex; i++) { old_data[i] = -999; } + if (fullScreenCell >= 0 && fullScreenCell < 4) { +#ifdef SERIAL_DEBUG Serial.println(">>> [DB] initScreen: drawing fullScreenMeter"); +#endif fullScreenMeter(fullScreenCell, pidInCell[fullScreenCell]); } else if (currentScreenPage == 1) { +#ifdef SERIAL_DEBUG Serial.println(">>> [DB] initScreen: drawing drawTriTempScreen(0)"); +#endif drawTriTempScreen(0); + } else if (currentScreenPage == 2) { +#ifdef SERIAL_DEBUG + Serial.println(">>> [DB] initScreen: drawing drawGpsClockScreen(0)"); +#endif + drawGpsClockScreen(0); + } else if (currentScreenPage == 3) { +#ifdef SERIAL_DEBUG + Serial.println(">>> [DB] initScreen: drawing drawGpsNavScreen(0)"); +#endif + drawGpsNavScreen(0); } else { +#ifdef SERIAL_DEBUG Serial.println(">>> [DB] initScreen: drawing Page 0 quadrants"); +#endif tft.fillScreen(TFT_BLACK); // clear screen for (uint8_t c = 0; c < 4; c++) { uint8_t pid = pidInCell[c]; arcMeter4(c, pid); } drawQuadrantDividers(); - // Pagination indicator dots [● ○] - tft.fillCircle(153, 2, 2, getThemePrimaryColor()); // Page 1 active - tft.fillCircle(167, 2, 2, 0x5AEB); // Page 2 inactive + // Pagination indicator dots [● ○ ○ ○] (Page 1 active) + tft.fillCircle(139, 2, 2, getThemePrimaryColor()); // Page 1 active + tft.fillCircle(153, 2, 2, 0x5AEB); // Page 2 inactive + tft.fillCircle(167, 2, 2, 0x5AEB); // Page 3 inactive + tft.fillCircle(181, 2, 2, 0x5AEB); // Page 4 inactive } if (currentScreenPage == 0 && fullScreenCell == -1) { badgeEcoDrawn = false; updateEcoBadge(true); } - Serial.printf(">>> [DB] initScreen FINISHED OK (heap: %u)\n", ESP.getFreeHeap()); +#ifdef SERIAL_DEBUG + Serial.printf(">>> [DB] initScreen FINISHED OK (heap: %u)\n", + ESP.getFreeHeap()); +#endif } /*-------------------------------*/ -// get A B from response return in global A B variable; returns true only if valid OBD2 data bytes were parsed +// get A B from response return in global A B variable; returns true only if +// valid OBD2 data bytes were parsed bool getAB(String elm_rsp, String currentPid) { // 41 05 2A 3C 01> A = 0; B = 0; if (currentPid == "ATRV") { return true; } - if (elm_rsp.indexOf("NO DATA") != -1 || - elm_rsp.indexOf("UNABLE") != -1 || - elm_rsp.indexOf("ERROR") != -1 || - elm_rsp.indexOf("STOPPED") != -1 || - elm_rsp.indexOf("BUS BUSY") != -1 || - elm_rsp.indexOf("TIMEOUT") != -1 || - elm_rsp.indexOf("SEARCHING") != -1 || - elm_rsp.indexOf("?") != -1 || + if (elm_rsp.indexOf("NO DATA") != -1 || elm_rsp.indexOf("UNABLE") != -1 || + elm_rsp.indexOf("ERROR") != -1 || elm_rsp.indexOf("STOPPED") != -1 || + elm_rsp.indexOf("BUS BUSY") != -1 || elm_rsp.indexOf("TIMEOUT") != -1 || + elm_rsp.indexOf("SEARCHING") != -1 || elm_rsp.indexOf("?") != -1 || elm_rsp.indexOf("BUFFER FULL") != -1) { return false; } @@ -1636,8 +2162,10 @@ bool getAB(String elm_rsp, String currentPid) { // 41 05 2A 3C 01> elm_rsp.replace('\n', ' '); elm_rsp.replace('>', ' '); - // Extract expected PID hex byte (e.g. "0105" -> "05", "010C" -> "0C", "010D" -> "0D") - String pidByte = (currentPid.length() >= 4) ? currentPid.substring(2) : currentPid; + // Extract expected PID hex byte (e.g. "0105" -> "05", "010C" -> "0C", "010D" + // -> "0D") + String pidByte = + (currentPid.length() >= 4) ? currentPid.substring(2) : currentPid; pidByte.toUpperCase(); bool needB = (pidByte == "0C"); // RPM requires both A and B! @@ -1649,9 +2177,11 @@ bool getAB(String elm_rsp, String currentPid) { // 41 05 2A 3C 01> while (startIdx < elm_rsp.length() && elm_rsp.charAt(startIdx) == ' ') { startIdx++; } - if (startIdx >= elm_rsp.length()) break; + if (startIdx >= elm_rsp.length()) + break; int endIdx = elm_rsp.indexOf(' ', startIdx); - if (endIdx == -1) endIdx = elm_rsp.length(); + if (endIdx == -1) + endIdx = elm_rsp.length(); tokens[tokenCount++] = elm_rsp.substring(startIdx, endIdx); startIdx = endIdx + 1; } @@ -1678,11 +2208,13 @@ bool getAB(String elm_rsp, String currentPid) { // 41 05 2A 3C 01> } } - // 2. Compact string fallback (in case adapter sends responses without spaces, e.g. "410D32") + // 2. Compact string fallback (in case adapter sends responses without spaces, + // e.g. "410D32") String compact = ""; for (int i = 0; i < elm_rsp.length(); i++) { char c = elm_rsp.charAt(i); - if (c != ' ') compact += (char)toupper(c); + if (c != ' ') + compact += (char)toupper(c); } String targetPrefix = "41" + pidByte; int fIdx = compact.indexOf(targetPrefix); @@ -1691,7 +2223,8 @@ bool getAB(String elm_rsp, String currentPid) { // 41 05 2A 3C 01> if (needB) { if (dataIdx + 4 <= compact.length()) { A = strtol(compact.substring(dataIdx, dataIdx + 2).c_str(), NULL, 16); - B = strtol(compact.substring(dataIdx + 2, dataIdx + 4).c_str(), NULL, 16); + B = strtol(compact.substring(dataIdx + 2, dataIdx + 4).c_str(), NULL, + 16); return true; } } else { @@ -1718,7 +2251,31 @@ bool getAB(String elm_rsp, String currentPid) { // 41 05 2A 3C 01> return false; } // getAB -/*-------------------------------*/ + +// Update vehicle speed from GPS on quadrant 1 (Velocidad) and fullscreen if +// active +inline void updateGpsSpeedometer() { + if (!isGpsSpeedValid()) + return; + float spd = round(getGpsSpeed()); + if (spd > 0.0f) + lastActivityTime = millis(); + + // Only update if integer speed changed or was invalid + static float lastRenderedGpsSpd = -999.0f; + if (spd != lastRenderedGpsSpd || last_valid_data[1] <= -900.0f) { + lastRenderedGpsSpd = spd; + last_valid_data[1] = spd; + + int warn = warningValue[3].toInt(); + if (fullScreenCell == 1) { + plotFullScreen(1, spd, warn, 0, 160, false, "km/h"); + } else if (currentScreenPage == 0 && fullScreenCell == -1) { + plotArc4(1, spd, warn, 0, 160, false, "km/h"); + } + } +} + void updateMeter(uint8_t pidNo, String response) { // update parameter on screen if (pidNo >= maxpidIndex) return; @@ -1764,14 +2321,19 @@ void updateMeter(uint8_t pidNo, String response) { // update parameter on screen String s = response; s.trim(); int vIdx = s.indexOf('V'); - if (vIdx == -1) vIdx = s.indexOf('v'); - if (vIdx != -1) s = s.substring(0, vIdx); + if (vIdx == -1) + vIdx = s.indexOf('v'); + if (vIdx != -1) + s = s.substring(0, vIdx); int lastSpace = s.lastIndexOf(' '); - if (lastSpace != -1) s = s.substring(lastSpace + 1); + if (lastSpace != -1) + s = s.substring(lastSpace + 1); int lastCr = s.lastIndexOf('\r'); - if (lastCr != -1) s = s.substring(lastCr + 1); + if (lastCr != -1) + s = s.substring(lastCr + 1); int lastLf = s.lastIndexOf('\n'); - if (lastLf != -1) s = s.substring(lastLf + 1); + if (lastLf != -1) + s = s.substring(lastLf + 1); s.trim(); data = s.toFloat(); if (data <= 1.0f) { @@ -1791,8 +2353,9 @@ void updateMeter(uint8_t pidNo, String response) { // update parameter on screen if (isFailed) { pidFailCount[pidNo]++; - // Debounce automotriz: si un paquete cae por ruido en el bus J1850, NUNCA poner en cero la pantalla, - // simplemente mantener el valor anterior. Solo si falla 10 veces seguidas declarar perdida de contacto. + // Debounce automotriz: si un paquete cae por ruido en el bus J1850, NUNCA + // poner en cero la pantalla, simplemente mantener el valor anterior. Solo + // si falla 10 veces seguidas declarar perdida de contacto. if (pidFailCount[pidNo] >= 10) { if (formula == 3) { currentRpm = 0.0f; // Motor detenido / sin RPM @@ -1802,7 +2365,8 @@ void updateMeter(uint8_t pidNo, String response) { // update parameter on screen if (consecutiveEcuFails >= 6) { ignitionOn = false; updateEcoBadge(); - // Contacto cerrado sostenido: limpiar todos los instrumentos a "---" (sin datos) + // Contacto cerrado sostenido: limpiar todos los instrumentos a "---" + // (sin datos) for (uint8_t c = 0; c < 4; c++) { plotNoData(c); } @@ -1812,7 +2376,8 @@ void updateMeter(uint8_t pidNo, String response) { // update parameter on screen return; // SALE SIN TOCAR LA PANTALLA NI PONERLA EN CERO } - // Si la respuesta fue valida, reseteamos contadores de fallo y confirmamos contacto puesto + // Si la respuesta fue valida, reseteamos contadores de fallo y confirmamos + // contacto puesto pidFailCount[pidNo] = 0; consecutiveEcuFails = 0; ignitionOn = true; @@ -1864,7 +2429,13 @@ void updateMeter(uint8_t pidNo, String response) { // update parameter on screen break; case 7: data = A; // km/h (PID 010D Vehicle Speed) - if (data > 0.0f) lastActivityTime = millis(); + if (data > 0.0f) + lastActivityTime = millis(); + // If GPS speed is active and locked, prioritize GPS speed over OBD-II + if (isGpsSpeedValid()) { + bt_message = ""; + return; + } break; case 8: // Ya parseado y validado arriba en data @@ -1873,7 +2444,8 @@ void updateMeter(uint8_t pidNo, String response) { // update parameter on screen return; } - // Si el motor esta en contacto pero apagado (RPM == 0), mostrar "---" en RPM en vez de 0 + // Si el motor esta en contacto pero apagado (RPM == 0), mostrar "---" en RPM + // en vez de 0 if (formula == 3 && data <= 0.0f) { currentRpm = 0.0f; updateEcoBadge(); @@ -2103,7 +2675,8 @@ void handleAlarmSound(bool anyAlarmActive, bool flashTick = false) { } } -// Non-blocking handler to blink/flash arcs and numerical values on alarm (when in TFT_RED) +// Non-blocking handler to blink/flash arcs and numerical values on alarm (when +// in TFT_RED) void handleArcAlarmFlash() { uint32_t now = millis(); bool toggleTick = false; @@ -2124,7 +2697,8 @@ void handleArcAlarmFlash() { uint16_t col = getMeterColor(pidIdx, data, warn); bool inRed = (col == TFT_RED); bool isAlarmActive = alarmEnabled[pidIdx]; - if (inRed && isAlarmActive) anyInRedAlarm = true; + if (inRed && isAlarmActive) + anyInRedAlarm = true; bool digit = pidConfig[pidIdx][7].toInt(); String numStr = String(data, digit); @@ -2189,7 +2763,8 @@ void handleArcAlarmFlash() { uint16_t col = getMeterColor(pidIdx, data, warn); bool inRed = (col == TFT_RED); bool isAlarmActive = alarmEnabled[pidIdx]; - if (inRed && isAlarmActive) anyInRedAlarm = true; + if (inRed && isAlarmActive) + anyInRedAlarm = true; bool digit = pidConfig[pidIdx][7].toInt(); String numStr = String(data, digit); int x = cell4_x[cell]; @@ -2245,7 +2820,8 @@ void handleArcAlarmFlash() { } } } else { - // Page 1 (Tri-Temperature screen): check alarm conditions for acoustic warning without touching screen graphics + // Page 1 (Tri-Temperature screen): check alarm conditions for acoustic + // warning without touching screen graphics for (uint8_t cell = 0; cell < 4; cell++) { if (last_valid_data[cell] != -999) { uint8_t pidIdx = pidInCell[cell]; @@ -2254,7 +2830,8 @@ void handleArcAlarmFlash() { uint16_t col = getMeterColor(pidIdx, data, warn); bool inRed = (col == TFT_RED); bool isAlarmActive = alarmEnabled[pidIdx]; - if (inRed && isAlarmActive) anyInRedAlarm = true; + if (inRed && isAlarmActive) + anyInRedAlarm = true; } } } @@ -2288,7 +2865,8 @@ void updateBacklightFade() { // auto dim backlight function void autoDim() { if (brightnessIndex > 0) { - uint8_t manualVal = BRIGHTNESS_PWM_TABLE[brightnessIndex > 5 ? 5 : brightnessIndex]; + uint8_t manualVal = + BRIGHTNESS_PWM_TABLE[brightnessIndex > 5 ? 5 : brightnessIndex]; if (targetBacklight != manualVal) { targetBacklight = manualVal; } diff --git a/sketch/obd2_gauge/obd2_gauge.ino b/sketch/obd2_gauge/obd2_gauge.ino index 7925fd7..130cc47 100644 --- a/sketch/obd2_gauge/obd2_gauge.ino +++ b/sketch/obd2_gauge/obd2_gauge.ino @@ -5,30 +5,30 @@ * Esquema de partición: huge_app (3MB APP / No OTA / LittleFS) *=============================================================================*/ -//Intermal temperature sensor function declaration +// Intermal temperature sensor function declaration #ifdef __cplusplus extern "C" { #endif - uint8_t temprature_sens_read(); +uint8_t temprature_sens_read(); #ifdef __cplusplus } #endif -uint8_t temprature_sens_read(); //declare intermal temperature sensor functio +uint8_t temprature_sens_read(); // declare intermal temperature sensor functio -//Macro -#define array_length(x) (sizeof(x) / sizeof(x[0])) //macro to calculate array length +// Macro +#define array_length(x) \ + (sizeof(x) / sizeof(x[0])) // macro to calculate array length -//library load -#include //save permanent data -Preferences pref; //create preference +// library load +#include //save permanent data +Preferences pref; // create preference #include -#include //Hardware-specific library +#include //Hardware-specific library - -//Pin configuration -#define SCA_PIN 21 //not used same pin with TFT_BL -#define SCL_PIN 22 //not used (avaialable) CAN_TX -#define ADC_PIN 35 //not used (available) CAN_RX +// Pin configuration +#define SCA_PIN 21 // not used same pin with TFT_BL +#define SCL_PIN 22 // not used (avaialable) CAN_TX +#define ADC_PIN 35 // not used (available) CAN_RX // Verified Physical Hardware Mapping on ESP32-2432S028: // GPIO 4 = RED LED // GPIO 16 = GREEN LED @@ -40,18 +40,19 @@ Preferences pref; //create preference #define LED_RED_CH 4 #define LED_GREEN_CH 5 #define LED_BLUE_CH 6 -#define LDR_PIN 34 //LDR sensor -#define BUZZER_PIN 26 //speaker -#define SELECTOR_PIN 27 //push button +#define LDR_PIN 34 // LDR sensor +#define BUZZER_PIN 26 // speaker +#define SELECTOR_PIN 27 // push button // setting PWM properties #define backlightChannel 0 #define buzzerChannel 2 -//DISPLAY +// DISPLAY TFT_eSPI tft = TFT_eSPI(); +TFT_eSprite sprTrans(&tft); +bool sprTransAllocated = false; #define TFT_GREY 0x5AEB - // ELM327 PIDs Configuration /*------ PIDS total 4 ------------------- Array data -> { Label , unit, pid, fomula, min, max, skip, digit, warn } @@ -66,38 +67,53 @@ digit = want to show digit or not warn = default warning value */ const String pidConfig[5][9] = { - //[pid][data] - { "Temperatura", "`C", "0105", "1", "60", "120", "2", "0", "110" }, //0 = Temperatura (0105) - { "RPM", "rpm", "010C", "3", "0", "6000", "0", "0", "5000" }, //1 = RPM (010C) - { "Voltaje", "V", "ATRV", "8", "10", "16", "1", "1", "15" }, //2 = Voltaje Bateria (ATRV) - { "Velocidad", "km/h", "010D", "7", "0", "160", "0", "0", "130" }, //3 = Velocidad (010D) - { "Carga", "%", "0104", "2", "0", "100", "0", "0", "85" } //4 = Carga Motor (0104) + //[pid][data] + {"Temperatura", "`C", "0105", "1", "60", "120", "2", "0", + "110"}, // 0 = Temperatura (0105) + {"RPM", "rpm", "010C", "3", "0", "6000", "0", "0", + "5000"}, // 1 = RPM (010C) + {"Voltaje", "V", "ATRV", "8", "10", "16", "1", "1", + "15"}, // 2 = Voltaje Bateria (ATRV) + {"Velocidad", "km/h", "010D", "7", "0", "160", "0", "0", + "130"}, // 3 = Velocidad (010D) + {"Carga", "%", "0104", "2", "0", "100", "0", "0", + "85"} // 4 = Carga Motor (0104) }; -//hold warning value -String warningValue[5] = { "110", "5000", "15.0", "130", "85" }; -bool alarmEnabled[5] = { true, true, true, true, false }; +// hold warning value +String warningValue[5] = {"110", "5000", "15.0", "130", "85"}; +bool alarmEnabled[5] = {true, true, true, true, false}; -// Buzzer volume control table (0: Silencio, 1: Ultra Bajo, 2: Muy Bajo, 3: Bajo/Casio, 4: Medio, 5: Alto) -const uint16_t VOL_DUTY_TABLE[6] = { 0, 2, 8, 25, 80, 250 }; -const char* const VOL_NAME_TABLE[6] = { "SILENCIO", "ULTRA BAJO", "MUY BAJO", "BAJO (CASIO)", "MEDIO", "ALTO" }; +// Buzzer volume control table (0: Silencio, 1: Ultra Bajo, 2: Muy Bajo, 3: +// Bajo/Casio, 4: Medio, 5: Alto) +const uint16_t VOL_DUTY_TABLE[6] = {0, 2, 8, 25, 80, 250}; +const char *const VOL_NAME_TABLE[6] = {"SILENCIO", "ULTRA BAJO", "MUY BAJO", + "BAJO (CASIO)", "MEDIO", "ALTO"}; uint8_t buzzerVolumeLevel = 3; // Default 3: Bajo (Casio suave) -uint8_t beepSoundType = 0; // Default 0: Casio Chirp (0: Casio Chirp, 1: Click Corto, 2: Tono Suave, 3: Tick Digital, 4: Doble Pip, 5: Campana Retro) -uint8_t alarmVolumeLevel = 3; // Default 3: Bajo/Casio (Volumen independiente de alarma) -uint8_t alarmSoundType = 0; // Default 0: Casio Doble (0: Casio Doble, 1: Sincronizado, 2: Bitonal, 3: Triple Urgente) -uint8_t autoSleepIndex = 2; // Default 2: 1 min (0: OFF, 1: 30s, 2: 1m, 3: 2m, 4: 5m) +uint8_t beepSoundType = + 0; // Default 0: Casio Chirp (0: Casio Chirp, 1: Click Corto, 2: Tono Suave, + // 3: Tick Digital, 4: Doble Pip, 5: Campana Retro) +uint8_t alarmVolumeLevel = + 3; // Default 3: Bajo/Casio (Volumen independiente de alarma) +uint8_t alarmSoundType = 0; // Default 0: Casio Doble (0: Casio Doble, 1: + // Sincronizado, 2: Bitonal, 3: Triple Urgente) +uint8_t autoSleepIndex = + 2; // Default 2: 1 min (0: OFF, 1: 30s, 2: 1m, 3: 2m, 4: 5m) uint32_t lastActivityTime = 0; // Inactivity tracker for auto-sleep -bool wokeFromSleep = false; // Flag set when waking from sleep to resume Bluetooth +bool wokeFromSleep = + false; // Flag set when waking from sleep to resume Bluetooth -// Screen brightness settings (0: AUTO LDR, 1: 5%, 2: 15%, 3: 35%, 4: 65%, 5: 100%) -uint8_t brightnessIndex = 0; // Default 0: AUTO -const uint8_t BRIGHTNESS_PWM_TABLE[6] = { 255, 12, 40, 90, 165, 255 }; -const char* const BRIGHTNESS_NAME_TABLE[6] = { "AUTO", "5%", "15%", "35%", "65%", "100%" }; +// Screen brightness settings (0: AUTO LDR, 1: 5%, 2: 15%, 3: 35%, 4: 65%, 5: +// 100%) +uint8_t brightnessIndex = 0; // Default 0: AUTO +const uint8_t BRIGHTNESS_PWM_TABLE[6] = {255, 12, 40, 90, 165, 255}; +const char *const BRIGHTNESS_NAME_TABLE[6] = {"AUTO", "5%", "15%", + "35%", "65%", "100%"}; // Instrument Cluster & Menu Themes struct GaugeTheme { - const char* name; - const char* desc; + const char *name; + const char *desc; uint16_t primaryColor; // Primary accent, borders, active glow, scroll thumb uint16_t accentColor; // Header gradient start, button resting background uint16_t divColor; // 4-Quadrant divider crosshair line @@ -109,38 +125,51 @@ struct GaugeTheme { }; const GaugeTheme GAUGE_THEMES[6] = { - { "Azul Cian", "Digital Moderno", 0x07FF, 0x03B9, 0x0254, 0x0110, 0x01D3, 0, 180, 255 }, // 0: Vivid Electric Cyan - { "Verde Sport", "Cockpit Clasico", 0x07E0, 0x0446, 0x0283, 0x0141, 0x0205, 0, 255, 30 }, // 1: Vivid Emerald Green - { "Ambar BMW", "Nocturno M-Power", 0xFDE0, 0x93C0, 0x5240, 0x2120, 0x41E0, 255, 120, 0 }, // 2: Vivid M-Power Glowing Amber - { "Blanco Puro", "OLED Precision", 0xFFFF, 0x6B6D, 0x4228, 0x18E3, 0x2965, 220, 230, 255 }, // 3: Pure Crisp White - { "Naranja Sport","McLaren Papaya", 0xFD20, 0x9260, 0x5160, 0x20A0, 0x4140, 255, 80, 0 }, // 4: Vivid Papaya Orange (reemplaza rojo) - { "Cyber Violet", "Neon Nightrunner", 0xD1BF, 0x7119, 0x40CE, 0x1846, 0x2869, 200, 0, 255 } // 5: Vivid Electric Violet + {"Azul Cian", "Digital Moderno", 0x07FF, 0x03B9, 0x0254, 0x0110, 0x01D3, 0, + 180, 255}, // 0: Vivid Electric Cyan + {"Verde Sport", "Cockpit Clasico", 0x07E0, 0x0446, 0x0283, 0x0141, 0x0205, + 0, 255, 30}, // 1: Vivid Emerald Green + {"Ambar BMW", "Nocturno M-Power", 0xFDE0, 0x93C0, 0x5240, 0x2120, 0x41E0, + 255, 120, 0}, // 2: Vivid M-Power Glowing Amber + {"Blanco Puro", "OLED Precision", 0xFFFF, 0x6B6D, 0x4228, 0x18E3, 0x2965, + 220, 230, 255}, // 3: Pure Crisp White + {"Naranja Sport", "McLaren Papaya", 0xFD20, 0x9260, 0x5160, 0x20A0, 0x4140, + 255, 80, 0}, // 4: Vivid Papaya Orange (reemplaza rojo) + {"Cyber Violet", "Neon Nightrunner", 0xD1BF, 0x7119, 0x40CE, 0x1846, 0x2869, + 200, 0, 255} // 5: Vivid Electric Violet }; uint8_t themeIndex = 0; // Default 0: Azul Cian +uint8_t transitionType = + 0; // 0: Deslizamiento (Slide), 1: Desvanecimiento (Fade) inline uint16_t getThemePrimaryColor() { - if (themeIndex >= 6) return 0x07FF; + if (themeIndex >= 6) + return 0x07FF; return GAUGE_THEMES[themeIndex].primaryColor; } inline uint16_t getThemeAccentColor() { - if (themeIndex >= 6) return 0x234F; + if (themeIndex >= 6) + return 0x234F; return GAUGE_THEMES[themeIndex].accentColor; } inline uint16_t getThemeDivColor() { - if (themeIndex >= 6) return 0x3412; + if (themeIndex >= 6) + return 0x3412; return GAUGE_THEMES[themeIndex].divColor; } inline uint16_t getThemeDarkColor() { - if (themeIndex >= 6) return 0x1105; + if (themeIndex >= 6) + return 0x1105; return GAUGE_THEMES[themeIndex].darkColor; } inline uint16_t getThemeBgColor() { - if (themeIndex >= 6) return 0x19C8; + if (themeIndex >= 6) + return 0x19C8; return GAUGE_THEMES[themeIndex].bgColor; } @@ -167,9 +196,9 @@ void setRearLedOff() { } void updateRearLedTheme() { - if (themeIndex >= 6) return; - setRearLedColor(GAUGE_THEMES[themeIndex].ledR, - GAUGE_THEMES[themeIndex].ledG, + if (themeIndex >= 6) + return; + setRearLedColor(GAUGE_THEMES[themeIndex].ledR, GAUGE_THEMES[themeIndex].ledG, GAUGE_THEMES[themeIndex].ledB); } @@ -179,10 +208,26 @@ void updateRearLedTheme() { 2 - Voltaje 3 - Velocidad */ -const uint8_t pidInCell[4] = { 0, 3, 1, 2 }; // 4 Cuadrantes: 0=Refrigerante, 1=Velocidad, 2=Revoluciones, 3=Voltaje +const uint8_t pidInCell[4] = { + 0, 3, 1, + 2}; // 4 Cuadrantes: 0=Refrigerante, 1=Velocidad, 2=Revoluciones, 3=Voltaje -int8_t fullScreenCell = -1; // -1 = 4 cuadrantes / multi-pagina, 0..3 = pantalla completa de ese cuadrante -int8_t currentScreenPage = 0; // 0 = 4 cuadrantes, 1 = Tri-Temperatura (Agua + Int + Ext) +int8_t fullScreenCell = -1; // -1 = 4 cuadrantes / multi-pagina, 0..3 = pantalla + // completa de ese cuadrante +int8_t currentScreenPage = + 0; // 0 = 4 cuadrantes, 1 = Tri-Temperatura, 2 = Reloj GPS / Fecha, 3 = + // Telemetria GPS (Altitud, Rumbo, Sats, Coordenadas) + +inline void setScreenPage(int8_t page) { + if (page < 0) + page = 0; + if (page > 3) + page = 3; + currentScreenPage = page; + pref.begin("setting", false); + pref.putChar("scr_page", page); + pref.end(); +} float tempEngineCoolant = 88.0f; // Temperatura de refrigerante de motor (OBD2) float tempInterior = 22.5f; // Temperatura interior habitáculo (DS18B20 #1) float tempExterior = 16.8f; // Temperatura exterior ambiente (DS18B20 #2) @@ -190,39 +235,43 @@ uint8_t dsSensorCount = 0; // Cantidad de sensores 1-Wire detectados // User configuration here to change display > /*---------------------------*/ -//if NO DATA value will set to 0 to skip reading -uint8_t layout = 0; //pidInCelltype 0-4 EEPROM 0x00 -const uint8_t max_layout = 1; //single screen mode -uint8_t pidIndex = 0; //point to PID List -const uint8_t maxpidIndex = array_length(pidConfig); //total 7 pids in picConfig -bool prompt = false; //bt elm ready flag -bool skip = false; //pid skip flag -uint8_t A = 0; //get A -uint8_t B = 0; //get B +// if NO DATA value will set to 0 to skip reading +uint8_t layout = 0; // pidInCelltype 0-4 EEPROM 0x00 +const uint8_t max_layout = 1; // single screen mode +uint8_t pidIndex = 0; // point to PID List +const uint8_t maxpidIndex = array_length(pidConfig); // total 7 pids in + // picConfig +bool prompt = false; // bt elm ready flag +bool skip = false; // pid skip flag +uint8_t A = 0; // get A +uint8_t B = 0; // get B -//Global variable -const uint32_t SLEEP_DURATION = 180 * 1000000; //180 sec* us = 3 min +// Global variable +const uint32_t SLEEP_DURATION = 180 * 1000000; // 180 sec* us = 3 min const uint8_t MAX_TERMINAL_LINES = 13; -String line[MAX_TERMINAL_LINES]; //buffer for each line in terminal function +String line[MAX_TERMINAL_LINES]; // buffer for each line in terminal function uint8_t terminalLineCount = 0; -bool dim = false; //back light dim flag -const uint8_t LIGHT_LEVEL = 40; //backlight control by light level Higher = darker light +bool dim = false; // back light dim flag +const uint8_t LIGHT_LEVEL = + 40; // backlight control by light level Higher = darker light - -//BLUETOOTH -String bt_message = ""; //bluetooth message buffer -String se_message = ""; //serial port message buffer -String deviceName[8] = { "", "", "", "", "", "", "", "" }; //discoveryBT name -String deviceAddr[8] = { "", "", "", "", "", "", "", "" }; //discover BT addr -uint8_t btDeviceCount = 0; //discovered bluetooth devices counter -#define BT_DISCOVER_TIME 5000 //bluetooth discoery time -esp_bd_addr_t client_addr = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; //obdII mac addr -esp_bd_addr_t recent_client_addr = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; //keep last btaddr in RTC memory -const String client_name = "OBDII"; //adaptor name to search -esp_spp_sec_t sec_mask = ESP_SPP_SEC_NONE; // or ESP_SPP_SEC_ENCRYPT|ESP_SPP_SEC_AUTHENTICATE to request pincode confirmation -esp_spp_role_t role = ESP_SPP_ROLE_SLAVE; // or ESP_SPP_ROLE_MASTER +// BLUETOOTH +String bt_message = ""; // bluetooth message buffer +String deviceName[8] = {"", "", "", "", "", "", "", ""}; // discoveryBT name +String deviceAddr[8] = {"", "", "", "", "", "", "", ""}; // discover BT addr +uint8_t btDeviceCount = 0; // discovered bluetooth devices counter +#define BT_DISCOVER_TIME 5000 // bluetooth discoery time +esp_bd_addr_t client_addr = {0x00, 0x00, 0x00, + 0x00, 0x00, 0x00}; // obdII mac addr +esp_bd_addr_t recent_client_addr = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // keep last btaddr in RTC memory +const String client_name = "OBDII"; // adaptor name to search +esp_spp_sec_t sec_mask = + ESP_SPP_SEC_NONE; // or ESP_SPP_SEC_ENCRYPT|ESP_SPP_SEC_AUTHENTICATE to + // request pincode confirmation +esp_spp_role_t role = ESP_SPP_ROLE_SLAVE; // or ESP_SPP_ROLE_MASTER bool foundOBD2 = false; -BluetoothSerial BTSerial; //bluetooth serial device +BluetoothSerial BTSerial; // bluetooth serial device //---- Include Header File --- #include "image.h" @@ -233,7 +282,6 @@ XPT2046_TouchscreenSOFTSPI ts(XPT2046_CS); /*----------- global function ---------*/ - /*------------------*/ // Draw a single console boot entry with authentic status badges void drawTerminalEntry(int lineIdx, const String &rawText) { @@ -285,17 +333,24 @@ void drawTerminalEntry(int lineIdx, const String &rawText) { } } -// TERMINAL: Authentic system boot console style loading lines from top to bottom -void Terminal(String texts, uint16_t x = 0, uint16_t y = 0, uint16_t w = 320, uint16_t h = 240) { - if (texts.length() == 0) return; +// TERMINAL: Authentic system boot console style loading lines from top to +// bottom +void Terminal(String texts, uint16_t x = 0, uint16_t y = 0, uint16_t w = 320, + uint16_t h = 240) { + if (texts.length() == 0) + return; // Auto-format standard messages to console style if not already tagged if (!texts.startsWith("[")) { if (texts.indexOf("OK") != -1 || texts.indexOf("Success") != -1) { texts = "[ OK ] " + texts; - } else if (texts.indexOf("error") != -1 || texts.indexOf("failed") != -1 || texts.indexOf("not found") != -1) { + } else if (texts.indexOf("error") != -1 || texts.indexOf("failed") != -1 || + texts.indexOf("not found") != -1) { texts = "[FAILED] " + texts; - } else if (texts.indexOf("Scanning") != -1 || texts.indexOf("Iniciando") != -1 || texts.indexOf("Conectando") != -1 || texts.indexOf("Insert") != -1) { + } else if (texts.indexOf("Scanning") != -1 || + texts.indexOf("Iniciando") != -1 || + texts.indexOf("Conectando") != -1 || + texts.indexOf("Insert") != -1) { texts = "[ INFO ] " + texts; } else { texts = " " + texts; @@ -323,7 +378,8 @@ void Terminal(String texts, uint16_t x = 0, uint16_t y = 0, uint16_t w = 320, ui // Send AT command or PID and wait synchronously for '>' prompt character String sendATCommand(const String &cmd, uint32_t timeoutMs = 2000) { - while (BTSerial.available() > 0) BTSerial.read(); // flush RX buffer + while (BTSerial.available() > 0) + BTSerial.read(); // flush RX buffer BTSerial.print(cmd + "\r"); String resp = ""; uint32_t start = millis(); @@ -346,9 +402,12 @@ String sendATCommand(const String &cmd, uint32_t timeoutMs = 2000) { } //---- Include Header File -------------------- -//#include "image.h" +// #include "image.h" #include "bluetooth.h" #include "tri_temp.h" +#include "gps_module.h" +#include "gps_clock.h" +#include "gps_nav.h" #include "meter.h" #include "config.h" @@ -363,11 +422,14 @@ void initializeELM327() { Terminal("[ OK ] Dispositivo: " + rZ); } - // 2. Base Configuration: Echo OFF, Linefeeds OFF, Headers OFF, Standard Adaptive Timing - sendATCommand("ATE0", 1000); // Echo OFF - sendATCommand("ATL0", 1000); // Linefeeds OFF - sendATCommand("ATH0", 1000); // Headers OFF - sendATCommand("ATAT1", 1000); // Adaptive timing normal (avoids timeouts on slower Ford ECUs) + // 2. Base Configuration: Echo OFF, Linefeeds OFF, Headers OFF, Standard + // Adaptive Timing + sendATCommand("ATE0", 1000); // Echo OFF + sendATCommand("ATL0", 1000); // Linefeeds OFF + sendATCommand("ATH0", 1000); // Headers OFF + sendATCommand( + "ATAT1", + 1000); // Adaptive timing normal (avoids timeouts on slower Ford ECUs) // 3. Test Battery Voltage via ATRV (direct from OBD pin 16) String rVolt = sendATCommand("ATRV", 1500); @@ -375,9 +437,11 @@ void initializeELM327() { Terminal("[ OK ] Bateria: " + rVolt); } - // 4. Conectar directamente al protocolo del auto (Ford J1850 PWM - Protocolo 1) + // 4. Conectar directamente al protocolo del auto (Ford J1850 PWM - Protocolo + // 1) Terminal("[ INFO ] Conectando Ford J1850 PWM (SCP)..."); - sendATCommand("ATSP1", 1000); // Forzar protocolo 1 directamente (evita busqueda lenta) + sendATCommand("ATSP1", + 1000); // Forzar protocolo 1 directamente (evita busqueda lenta) String r0100 = sendATCommand("0100", 5000); if (r0100.indexOf("41 00") != -1) { @@ -396,13 +460,63 @@ void initializeELM327() { prompt = true; } +// Background Bluetooth connection task on FreeRTOS Core 0 (100% non-blocking +// for UI & GPS) +TaskHandle_t btTaskHandle = NULL; +volatile bool btConnectedTrigger = false; +volatile bool systemSleeping = false; + +void btBackgroundTask(void *pvParameters) { + while (true) { + if (!systemSleeping && !foundOBD2) { + bool hasRecentAddr = false; + for (uint8_t i = 0; i < 6; i++) { + if (recent_client_addr[i] != 0) { + hasRecentAddr = true; + break; + } + } + if (hasRecentAddr) { + setRearLedColor(0, 0, 200); + BTSerial.disconnect(); + vTaskDelay(pdMS_TO_TICKS(100)); + if (BTSerial.connect(recent_client_addr, 0, sec_mask, role)) { + if (BTSerial.connected() && !systemSleeping) { + sendATCommand("ATE0", 800); + sendATCommand("ATL0", 800); + sendATCommand("ATH0", 800); + sendATCommand("ATAT1", 800); + sendATCommand("ATSP1", 800); + String r0100 = sendATCommand("0100", 2500); + ignitionOn = (r0100.indexOf("41 00") != -1); + foundOBD2 = true; + prompt = true; + bt_message = ""; + updateRearLedTheme(); + btConnectedTrigger = + true; // Signal Core 1 (UI thread) to switch screen! + } else { + BTSerial.disconnect(); + setRearLedOff(); + foundOBD2 = false; + } + } else { + setRearLedOff(); + foundOBD2 = false; + } + } + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(3500)); + } +} + //----SET UP ----------------------------------- void setup() { - //pin configuration - analogSetAttenuation(ADC_0db); // 0dB(1.0 ครั้ง) 0~800mV for LDR - pinMode(LDR_PIN, ANALOG); //ldr analog input read brightness - pinMode(BUZZER_PIN, OUTPUT); //speaker - pinMode(SELECTOR_PIN, INPUT_PULLUP); //button + // pin configuration + analogSetAttenuation(ADC_0db); // 0dB(1.0 ครั้ง) 0~800mV for LDR + pinMode(LDR_PIN, ANALOG); // ldr analog input read brightness + pinMode(BUZZER_PIN, OUTPUT); // speaker + pinMode(SELECTOR_PIN, INPUT_PULLUP); // button // Rear RGB LED PWM setup (Channels 4, 5, 6 - Timers 2 & 3) ledcSetup(LED_RED_CH, 5000, 8); ledcSetup(LED_GREEN_CH, 5000, 8); @@ -411,42 +525,49 @@ void setup() { ledcAttachPin(LED_GREEN_PIN, LED_GREEN_CH); ledcAttachPin(LED_BLUE_PIN, LED_BLUE_CH); setRearLedOff(); - //pwm setup - ledcSetup(buzzerChannel, 1500, 10); //buzzer 10 bit - ledcSetup(backlightChannel, 12000, 8); //backlight 8 bit - ledcAttachPin(BUZZER_PIN, buzzerChannel); //attach buzzer + // pwm setup + ledcSetup(buzzerChannel, 1500, 10); // buzzer 10 bit + ledcSetup(backlightChannel, 12000, 8); // backlight 8 bit + ledcAttachPin(BUZZER_PIN, buzzerChannel); // attach buzzer - //init communication - Serial.begin(115200); - //memory check + // memory check psramInit(); + +#ifdef SERIAL_DEBUG + Serial.begin(115200); Serial.println(F("\n---------------------------------------\n")); - Serial.printf("Free Internal Heap %d/%d\n", ESP.getFreeHeap(), ESP.getHeapSize()); - Serial.printf("Free SPIRam Heap %d/%d\n", ESP.getFreePsram(), ESP.getPsramSize()); - Serial.printf("ChipRevision %d, Cpu Freq %d, SDK Version %s\n", ESP.getChipRevision(), ESP.getCpuFreqMHz(), ESP.getSdkVersion()); - Serial.printf("Flash Size %d, Flash Speed %d\n", ESP.getFlashChipSize(), ESP.getFlashChipSpeed()); + Serial.printf("Free Internal Heap %d/%d\n", ESP.getFreeHeap(), + ESP.getHeapSize()); + Serial.printf("Free SPIRam Heap %d/%d\n", ESP.getFreePsram(), + ESP.getPsramSize()); + Serial.printf("ChipRevision %d, Cpu Freq %d, SDK Version %s\n", + ESP.getChipRevision(), ESP.getCpuFreqMHz(), + ESP.getSdkVersion()); + Serial.printf("Flash Size %d, Flash Speed %d\n", ESP.getFlashChipSize(), + ESP.getFlashChipSpeed()); Serial.println(F("\n---------------------------------------\n")); + Serial.printf(">>> [BOOT] Reset reason: %d, wakeup cause: %d\n", + esp_reset_reason(), esp_sleep_get_wakeup_cause()); Serial.println(F("<< OBD2 Gauge >>")); +#endif - - - //init TFT display + // init TFT display tft.init(); - tft.setRotation(1); //landcape + tft.setRotation(1); // landcape // Start SoftSPI for the touchscreen and init the touchscreen tsSoftSpi = new SoftSPI(XPT2046_MOSI, XPT2046_MISO, XPT2046_CLK); ts.begin(tsSoftSpi); ts.setRotation(1); - touch_calibrate(); // initialize / load touch calibration - //testTouch();//test touchscreen + touch_calibrate(); // initialize / load touch calibration + // testTouch();//test touchscreen - tft.setSwapBytes(true); // to display correct image colors + tft.setSwapBytes(true); // to display correct image colors - //backlight ledcAttachPin must be set after tft.init() - ledcAttachPin(TFT_BL, backlightChannel); //attach backlight - ledcWrite(backlightChannel, 255); //full bright - tft.fillScreen(TFT_BLACK); // Consola de arranque de sistema + // backlight ledcAttachPin must be set after tft.init() + ledcAttachPin(TFT_BL, backlightChannel); // attach backlight + ledcWrite(backlightChannel, 255); // full bright + tft.fillScreen(TFT_BLACK); // Consola de arranque de sistema terminalLineCount = 0; Terminal("[ OK ] ESP32-D0WD Xtensa LX6 Dual-Core @ 240MHz"); @@ -462,11 +583,11 @@ void setup() { Terminal("[ OK ] Input: XPT2046 Resistive Touch Calibrated"); delay(40); - //init variable + // init variable pref.begin("setting", false); /* Create a namespace called "setting" with read/write mode { pref.get...(key,default) -layout : UShort +layout : UShort showsystem: bool tempOffset : uint16_t warning value: uint16_t @@ -474,9 +595,11 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6] }*/ layout = 0; pref.putUShort("layout", 0); - for (uint8_t i = 0; i < maxpidIndex; i++) { //read warning value from pid name if not found load from default + for (uint8_t i = 0; i < maxpidIndex; + i++) { // read warning value from pid name if not found load from default warningValue[i] = pref.getString(pidConfig[i][0].c_str(), pidConfig[i][8]); - if (i == 0 && (warningValue[0] == "106" || warningValue[0] == "0" || warningValue[0] == "")) { + if (i == 0 && (warningValue[0] == "106" || warningValue[0] == "0" || + warningValue[0] == "")) { warningValue[0] = "110"; pref.putString(pidConfig[0][0].c_str(), "110"); } @@ -484,97 +607,180 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6] alarmEnabled[i] = pref.getBool(almKey.c_str(), (i < 4)); } buzzerVolumeLevel = pref.getUChar("b_vol", 3); - if (buzzerVolumeLevel > 5) buzzerVolumeLevel = 3; + if (buzzerVolumeLevel > 5) + buzzerVolumeLevel = 3; beepSoundType = pref.getUChar("b_type", 0); - if (beepSoundType > 5) beepSoundType = 0; + if (beepSoundType > 5) + beepSoundType = 0; alarmVolumeLevel = pref.getUChar("alm_vol", 3); - if (alarmVolumeLevel > 5) alarmVolumeLevel = 3; + if (alarmVolumeLevel > 5) + alarmVolumeLevel = 3; alarmSoundType = pref.getUChar("alm_type", 0); - if (alarmSoundType > 5) alarmSoundType = 0; + if (alarmSoundType > 5) + alarmSoundType = 0; autoSleepIndex = pref.getUChar("auto_sleep", 2); - if (autoSleepIndex > 4) autoSleepIndex = 2; + if (autoSleepIndex > 4) + autoSleepIndex = 2; brightnessIndex = pref.getUChar("b_bright", 0); - if (brightnessIndex > 5) brightnessIndex = 0; + if (brightnessIndex > 5) + brightnessIndex = 0; themeIndex = pref.getUChar("theme_idx", 0); - if (themeIndex > 5) themeIndex = 0; + if (themeIndex > 5) + themeIndex = 0; + transitionType = pref.getUChar("trans_type", 0); + if (transitionType > 1) + transitionType = 0; + currentScreenPage = pref.getChar("scr_page", 0); + if (currentScreenPage < 0 || currentScreenPage > 3) + currentScreenPage = 0; if (brightnessIndex > 0) { targetBacklight = BRIGHTNESS_PWM_TABLE[brightnessIndex]; currentBacklight = (float)targetBacklight; ledcWrite(backlightChannel, targetBacklight); } lastActivityTime = millis(); - pref.getBytes("recent_client", recent_client_addr, pref.getBytesLength("recent_client")); //read last bt address - + pref.getBytes("recent_client", recent_client_addr, + pref.getBytesLength("recent_client")); // read last bt address updateRearLedTheme(); // Sync rear RGB LED with loaded theme - Terminal("[ OK ] Settings: Theme " + String(GAUGE_THEMES[themeIndex].name) + " (LED RGB)"); + Terminal("[ OK ] Settings: Theme " + String(GAUGE_THEMES[themeIndex].name) + + " (LED RGB)"); + Terminal("[ OK ] Settings: Transicion " + + String(transitionType == 0 ? "Deslizamiento" : "Desvanecimiento")); + delay(40); + + // Initialize persistent transition sprite (pre-allocated once at boot in clean heap) + if (sprTrans.createSprite(320, 40) != nullptr) { + sprTransAllocated = true; + sprTrans.setTextFont(1); + sprTrans.setFreeFont(NULL); + sprTrans.loadFont(DSEG7_Modern_BoldItalic_34); + sprTrans.fontLoaded = false; + Terminal("[ OK ] UI: Persistent 25KB Transition Engine Online"); + } else { + Terminal("[ WARN ] UI: Transition buffer alloc failed"); + } delay(40); // Initialize 1-Wire DS18B20 digital temperature bus initTriTemp(); if (dsSensorCount > 0) { - Terminal("[ OK ] 1-Wire: " + String(dsSensorCount) + "x DS18B20 Temp Sensors on GPIO 22"); + Terminal("[ OK ] 1-Wire: " + String(dsSensorCount) + + "x DS18B20 Temp Sensors on GPIO 22"); } else { Terminal("[ OK ] 1-Wire: DS18B20 Bus Online (GPIO 22)"); } delay(40); - //start bluetooth + // Initialize GPS UART0 on GPIO 3 (RX) and GPIO 1 (TX) + setupGPS(); + Terminal("[ OK ] GPS: N32+ / U-blox UART Online (GPIO 3/1 @ 9600)"); + delay(40); + + // Initialize Bluetooth Controller stack Terminal("[ INFO ] Initializing Bluetooth Controller..."); if (!BTSerial.begin("OBD2 Gauge", true)) { +#ifdef SERIAL_DEBUG Serial.println(F("Bluetooth..error!")); +#endif Terminal("[FAILED] Bluetooth controller initialization error!"); - abort(); } else { +#ifdef SERIAL_DEBUG Serial.println(F("Bluetooth..OK")); +#endif Terminal("[ OK ] Bluetooth SPP Master stack online"); } #ifdef SKIP_CONNECTION foundOBD2 = true; +#else + foundOBD2 = false; #endif - //Connect to ELM327 - connectLastOBDII(); //try connect last BT - while (!foundOBD2) { //not success try scan and connect another OBD2 - scanBTdevice(); - autoDim(); //auto backlight handle + // Lanzar tarea FreeRTOS en Core 0 para conexion y reconexion Bluetooth + // continua en segundo plano (100% no bloqueante) + xTaskCreatePinnedToCore(btBackgroundTask, "btTask", 4096, NULL, 1, + &btTaskHandle, 0); - if (digitalRead(SELECTOR_PIN) == LOW) { //button pressed - delay(50); - if (digitalRead(SELECTOR_PIN) == LOW) { - configMenu(); - waitForTouchRelease(); + // Iniciar DIRECTA e INSTANTANEAMENTE en la pantalla del Reloj GPS y Fecha + // (Pagina 2) + currentScreenPage = 2; + setScreenPage(currentScreenPage); + fullScreenCell = -1; + initScreen(true); + beepbeep(); + prompt = true; + bt_message = ""; + //---------------------- +} // setup + +/*###################################*/ +void loop() { + updateBacklightFade(); // non-blocking smooth backlight fade handler + handleArcAlarmFlash(); // non-blocking arc alarm flash handler (blinks when in + // TFT_RED) + + // Trigger automatico: Cuando el Bluetooth conecta en segundo plano (Core 0), + // pasar a pantalla OBD! + if (btConnectedTrigger) { + btConnectedTrigger = false; + setScreenPage(0); // Cambiar a tablero de 4 cuadrantes OBD! + fullScreenCell = -1; + initScreen(true); + touchBeep(); + lastActivityTime = millis(); + } + + // Evaluacion inicial: Si tras el arranque no encuentra ni Bluetooth ni GPS, + // pasar a la pantalla de las Temperaturas (Pagina 1) + static bool bootSearchEvaluated = false; + if (!bootSearchEvaluated && millis() >= 7000) { + bootSearchEvaluated = true; + bool isBt = BTSerial.connected() && foundOBD2; + if (!isBt && !isGpsSynced()) { + if (currentScreenPage == 2 && fullScreenCell == -1) { + setScreenPage(1); // Ir a la pantalla de las Temperaturas (Pagina 1) + fullScreenCell = -1; + initScreen(true); + lastActivityTime = millis(); } } } - //Initialize ELM327 - initializeELM327(); - //---------------------- -} //setup - -/*###################################*/ -void loop() { - updateBacklightFade(); // non-blocking smooth backlight fade handler - handleArcAlarmFlash(); // non-blocking arc alarm flash handler (blinks when in TFT_RED) - - // Non-blocking periodic reading of DS18B20 digital temperature sensors (every 1s) + // Non-blocking periodic reading of DS18B20 digital temperature sensors (every + // 1s) static uint32_t lastDsReadTime = 0; if (millis() - lastDsReadTime >= 1000) { lastDsReadTime = millis(); updateTriTempSensors(false); } + // Continuous background processing of GPS NMEA stream and speedometer + // calculation + processGPS(); + updateGpsSpeedometer(); + updateGpsIconAnim(); + + // Non-blocking live refresh for resting Page 2 (GPS Clock & Date) + if (currentScreenPage == 2 && fullScreenCell == -1) { + updateGpsClockScreen(false); + } + + // Non-blocking live refresh for resting Page 3 (GPS Navigation & Telemetry) + if (currentScreenPage == 3 && fullScreenCell == -1) { + updateGpsNavScreen(false); + } + if (wokeFromSleep) { wokeFromSleep = false; lastActivityTime = millis(); - resumeBluetoothConnection(); + initScreen(true); } // SCAN TOUCHSCREEN & BUTTON // - Double click: switch screen page (Page 0 <-> Page 1) with slide animation - // - Single click: toggle fullscreen for tapped quadrant (or refresh sensors on Tri-Temp) + // - Single click: toggle fullscreen for tapped quadrant (or refresh sensors + // on Tri-Temp) // - Long press (> 1s): open configuration menu // - Physical selector button: open configuration menu uint16_t touchX = 0, touchY = 0; @@ -591,19 +797,16 @@ void loop() { static uint16_t lastTouchX = 0, lastTouchY = 0; static bool longPressHandled = false; - // Double-tap state machine - static bool waitingForDoubleTap = false; - static uint32_t firstTapReleaseTime = 0; - static uint16_t firstTapX = 0, firstTapY = 0; - if (btnPressed) { delay(50); if (digitalRead(SELECTOR_PIN) == LOW) { - waitingForDoubleTap = false; touchBeep(); configMenu(); waitForTouchRelease(); lastActivityTime = millis(); + if (wokeFromSleep) { + wokeFromSleep = false; + } initScreen(); } } else if (isTouched) { @@ -619,19 +822,53 @@ void loop() { lastTouchX = touchX; lastTouchY = touchY; - // Long press check: finger held stationary for > 1000ms - int16_t currentDistX = abs((int16_t)lastTouchX - (int16_t)touchStartX); - int16_t currentDistY = abs((int16_t)lastTouchY - (int16_t)touchStartY); + int16_t liveDeltaX = (int16_t)lastTouchX - (int16_t)touchStartX; + int16_t liveDeltaY = (int16_t)lastTouchY - (int16_t)touchStartY; - if (!longPressHandled && (millis() - touchStartTime > 1000) && currentDistX < 35 && currentDistY < 35) { - longPressHandled = true; - waitingForDoubleTap = false; - touchBeep(); // Confirmation chirp - configMenu(); - waitForTouchRelease(); - lastActivityTime = millis(); - initScreen(); - isTouching = false; + // FAST LIVE SWIPE DETECTION: + // Trigger immediately as soon as finger moves horizontally >= 28px + // (disabled in full-screen mode) + if (!longPressHandled) { + if (fullScreenCell == -1 && liveDeltaX < -28 && abs(liveDeltaY) < 85) { + // SWIPE LEFT: Transition to next page (0 -> 1 -> 2 -> 3 -> 0) + longPressHandled = true; + touchBeep(); + uint8_t nextPage = (currentScreenPage + 1) % 4; + performScreenTransition(true, currentScreenPage, nextPage); + setScreenPage(nextPage); + initScreen(false); // Seamless: keep transition frame without blanking + // to black + waitForTouchRelease(); + isTouching = false; + lastActivityTime = millis(); + } else if (fullScreenCell == -1 && liveDeltaX > 28 && + abs(liveDeltaY) < 85) { + // SWIPE RIGHT: Transition to previous page (0 -> 3 -> 2 -> 1 -> 0) + longPressHandled = true; + touchBeep(); + uint8_t nextPage = + (currentScreenPage == 0) ? 3 : (currentScreenPage - 1); + performScreenTransition(false, currentScreenPage, nextPage); + setScreenPage(nextPage); + initScreen(false); // Seamless: keep transition frame without blanking + // to black + waitForTouchRelease(); + isTouching = false; + lastActivityTime = millis(); + } else if ((millis() - touchStartTime > 1000) && abs(liveDeltaX) < 30 && + abs(liveDeltaY) < 30) { + // Long press (> 1s) held stationary: Open Config Menu immediately! + longPressHandled = true; + touchBeep(); // Confirmation chirp + configMenu(); + waitForTouchRelease(); + lastActivityTime = millis(); + if (wokeFromSleep) { + wokeFromSleep = false; + } + initScreen(); + isTouching = false; + } } } } else { @@ -641,201 +878,170 @@ void loop() { uint32_t pressDuration = millis() - touchStartTime; if (!longPressHandled) { - int16_t deltaX = abs((int16_t)lastTouchX - (int16_t)touchStartX); - int16_t deltaY = abs((int16_t)lastTouchY - (int16_t)touchStartY); + int16_t deltaX = (int16_t)lastTouchX - (int16_t)touchStartX; + int16_t deltaY = (int16_t)lastTouchY - (int16_t)touchStartY; - // Anti-noise: require at least 30ms of contact and displacement < 40px (clean tap) - if (pressDuration >= 30 && deltaX < 40 && deltaY < 40) { - touchBeep(); // Crisp acoustic feedback on confirmed tap! - - if (waitingForDoubleTap && (millis() - firstTapReleaseTime <= 400)) { - // DOUBLE CLICK DETECTED: Switch Screen Page! - waitingForDoubleTap = false; - Serial.println(">>> [DB] DOUBLE CLICK DETECTED!"); - Serial.printf(">>> [DB] Free heap: %u, MaxAlloc: %u\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); - - if (fullScreenCell != -1) { + // Quick flick swipe fallback in case finger was lifted very rapidly + // (disabled in full-screen mode) + if (fullScreenCell == -1 && deltaX < -25 && abs(deltaY) < 85) { + // SWIPE LEFT + longPressHandled = true; + touchBeep(); + uint8_t nextPage = (currentScreenPage + 1) % 4; + performScreenTransition(true, currentScreenPage, nextPage); + setScreenPage(nextPage); + initScreen(false); // Seamless: keep transition frame without blanking + // to black + waitForTouchRelease(); + isTouching = false; + lastActivityTime = millis(); + } else if (fullScreenCell == -1 && deltaX > 25 && abs(deltaY) < 85) { + // SWIPE RIGHT + longPressHandled = true; + touchBeep(); + uint8_t nextPage = + (currentScreenPage == 0) ? 3 : (currentScreenPage - 1); + performScreenTransition(false, currentScreenPage, nextPage); + setScreenPage(nextPage); + initScreen(false); // Seamless: keep transition frame without blanking + // to black + waitForTouchRelease(); + isTouching = false; + lastActivityTime = millis(); + } else if (pressDuration >= 30 && abs(deltaX) < 35 && + abs(deltaY) < 35) { + // Valid TAP detected! + touchBeep(); + if (currentScreenPage == 0) { + if (fullScreenCell == -1) { + // Switch to full screen for tapped quadrant + int quad = (lastTouchX < 160) ? ((lastTouchY < 120) ? 0 : 2) + : ((lastTouchY < 120) ? 1 : 3); + fullScreenCell = quad; + } else { + // Return to 4 quadrants view fullScreenCell = -1; } - - if (currentScreenPage == 0) { - Serial.println(">>> [DB] Transitioning Page 0 -> 1"); - animateSlideTransition(true); - Serial.println(">>> [DB] Slide done. Setting currentScreenPage = 1"); - currentScreenPage = 1; - Serial.println(">>> [DB] Calling initScreen()..."); - initScreen(); - Serial.println(">>> [DB] initScreen() completed."); - } else { - Serial.println(">>> [DB] Transitioning Page 1 -> 0"); - animateSlideTransition(false); - Serial.println(">>> [DB] Slide done. Setting currentScreenPage = 0"); - currentScreenPage = 0; - Serial.println(">>> [DB] Calling initScreen()..."); - initScreen(); - Serial.println(">>> [DB] initScreen() completed."); - } - waitForTouchRelease(); - lastActivityTime = millis(); - } else { - // First tap of a potential double click: start waiting window - waitingForDoubleTap = true; - firstTapReleaseTime = millis(); - firstTapX = lastTouchX; - firstTapY = lastTouchY; - Serial.printf(">>> [DB] First tap (X=%d, Y=%d), waiting for 2nd tap...\n", firstTapX, firstTapY); + initScreen(); + } else if (currentScreenPage == 1) { + // Tap on Tri-Temp screen: immediate sensor refresh + updateTriTempSensors(true); + } else if (currentScreenPage == 2) { + // Tap on GPS Clock screen: immediate time and telemetry refresh + updateGpsClockScreen(true); + } else if (currentScreenPage == 3) { + // Tap on GPS Navigation screen: immediate telemetry refresh + updateGpsNavScreen(true); } + waitForTouchRelease(); + lastActivityTime = millis(); } } } - - // Check if double-tap timeout has expired -> execute SINGLE CLICK action! - if (waitingForDoubleTap && (millis() - firstTapReleaseTime > 400)) { - waitingForDoubleTap = false; - - if (currentScreenPage == 0) { - if (fullScreenCell == -1) { - // Switch to full screen for tapped quadrant - int quad = (firstTapX < 160) ? ((firstTapY < 120) ? 0 : 2) : ((firstTapY < 120) ? 1 : 3); - fullScreenCell = quad; - } else { - // Return to 4 quadrants view - fullScreenCell = -1; - } - initScreen(); - } else { - // Tap on Tri-Temp screen: immediate sensor refresh - updateTriTempSensors(true); - } - lastActivityTime = millis(); - } } // AUTO-SLEEP BY INACTIVITY - // If auto-sleep is enabled, and screen is not being held, and engine is stopped (RPM <= 300) - if (autoSleepIndex > 0 && !isTouching && !btnPressed && !longPressHandled && !waitingForDoubleTap && currentRpm <= 300.0f) { - uint32_t timeoutMs = (uint32_t)AUTO_SLEEP_SEC_TABLE[autoSleepIndex] * 1000UL; + // If auto-sleep is enabled, and screen is not being held, and engine is + // stopped (RPM <= 300) + if (autoSleepIndex > 0 && !isTouching && !btnPressed && !longPressHandled && + currentRpm <= 300.0f) { + uint32_t timeoutMs = + (uint32_t)AUTO_SLEEP_SEC_TABLE[autoSleepIndex] * 1000UL; if (millis() - lastActivityTime >= timeoutMs) { enterLightSleep(); wokeFromSleep = false; // handled directly here lastActivityTime = millis(); - resumeBluetoothConnection(); + initScreen(true); } } #ifndef SKIP_CONNECTION - // Check Bluetooth connection state: if adapter disconnected or lost power, show "---" (no data) - if (!BTSerial.connected() || !foundOBD2) { - static uint32_t lastBtReconnect = 0; + // Check Bluetooth connection state: if adapter disconnected or lost power, + // switch immediately to GPS Clock Screen (Page 2) and show "---" on OBD + // parameters + static bool wasBtConnected = false; + bool isBtConnected = BTSerial.connected() && foundOBD2; + + // Si se pierde el enlace Bluetooth: si hay GPS pasa al Reloj (Pag 2), si no + // hay GPS pasa a Temperaturas (Pag 1) + if (!isBtConnected && wasBtConnected) { + wasBtConnected = false; currentRpm = 0.0f; ignitionOn = false; - for (uint8_t c = 0; c < 4; c++) { - plotNoData(c); - } - if (millis() - lastBtReconnect > 3000) { - lastBtReconnect = millis(); -#ifdef SERIAL_DEBUG - Serial.println(F("BT desconectado. Reintentando reconexion...")); -#endif - setRearLedColor(0, 0, 200); - BTSerial.disconnect(); - if (BTSerial.connect(recent_client_addr, 0, sec_mask, role)) { - uint8_t tries = 0; - while (!BTSerial.connected(500) && tries < 4) { - tries++; - } - if (BTSerial.connected()) { - sendATCommand("ATE0", 800); - sendATCommand("ATL0", 800); - sendATCommand("ATH0", 800); - sendATCommand("ATAT1", 800); - sendATCommand("ATSP1", 800); - String r0100 = sendATCommand("0100", 2500); - ignitionOn = (r0100.indexOf("41 00") != -1); - foundOBD2 = true; - prompt = true; - bt_message = ""; - updateRearLedTheme(); - initScreen(); - touchBeep(); - lastActivityTime = millis(); - } else { - BTSerial.disconnect(); - setRearLedOff(); - foundOBD2 = false; - } - } else { - setRearLedOff(); - foundOBD2 = false; + foundOBD2 = false; + uint8_t fallbackPage = isGpsSynced() ? 2 : 1; + setScreenPage(fallbackPage); + fullScreenCell = -1; + initScreen(true); + setRearLedOff(); + lastActivityTime = millis(); + return; + } + wasBtConnected = isBtConnected; + + if (!isBtConnected) { + currentRpm = 0.0f; + ignitionOn = false; + if (currentScreenPage == 0 || fullScreenCell != -1) { + for (uint8_t c = 0; c < 4; c++) { + if (c == 1 && isGpsSpeedValid()) + continue; // Mantener la velocidad de GPS en el velocimetro + plotNoData(c); } } + foundOBD2 = + false; // Core 0 background task will handle reconnection smoothly! return; } #endif //---------------------- - //BLUETOOTH read + // BLUETOOTH read static uint32_t lastPidSendTime = 0; - // Watchdog: If waiting for prompt for more than 5000ms, recover safely without touching screen + // Watchdog: If waiting for prompt for more than 5000ms, recover safely + // without touching screen if (!prompt && (millis() - lastPidSendTime > 5000)) { #ifdef SERIAL_DEBUG - Serial.printf("PID timeout for %s, recovering...\n", pidList[pidIndex].c_str()); + Serial.printf("PID timeout for %s, recovering...\n", + pidList[pidIndex].c_str()); #endif BTSerial.flush(); bt_message = ""; prompt = true; - skip = true; // IMPORTANT: skip updateMeter so timeout never puts zeros on screen! + skip = true; // IMPORTANT: skip updateMeter so timeout never puts zeros on + // screen! } while (BTSerial.available() > 0) { char incomingChar = BTSerial.read(); - if (incomingChar == '>') { //check prompt - BTSerial.flush(); //clear tx + if (incomingChar == '>') { // check prompt + BTSerial.flush(); // clear tx #ifdef SERIAL_DEBUG Serial.println(bt_message); #endif -#ifdef TERMINAL //for degbugging +#ifdef TERMINAL // for degbugging getAB(bt_message, pidList[pidIndex]); - Terminal(bt_message + "->" + String(A) + "," + String(B), 0, 48, 320, 191); + Terminal(bt_message + "->" + String(A) + "," + String(B), 0, 48, 320, + 191); #endif - prompt = true; //pid response ready to calculate & display + prompt = true; // pid response ready to calculate & display } else { - bt_message.concat(incomingChar); //keep elm327 respond + bt_message.concat(incomingChar); // keep elm327 respond } } - //---------------------- - //SERIAL PORT read - if (Serial.available() > 0) { - char incomingChar = Serial.read(); - //check CR/NL - if ((incomingChar != '\r') && (incomingChar != '\n')) { - se_message.concat(incomingChar); - //Serial.print(incomingChar,HEX); - } else { - if (se_message != "") { -#ifdef TERMINAL - Terminal(se_message, 0, 48, 320, 191); //display terminal -#endif - BTSerial.print(se_message + "\r"); - se_message = ""; - Serial.flush(); - } //if se_message - - } //else if incomingChar - } //if Serial.available - - //Send another PID to elm327 - if (prompt) { //> ELM327 ready! -> request next PID + // Send another PID to elm327 + if (prompt) { //> ELM327 ready! -> request next PID if (!skip && bt_message.length() > 0) { - updateMeter(pidIndex, bt_message); //update meter screen + updateMeter(pidIndex, bt_message); // update meter screen } bt_message = ""; if (!ignitionOn) { // Contacto cerrado / Standby: - // No bombardear el bus OBD de la ECU continuamente (evita que parpadee la luz verde del scanner y drena bateria). - // Solo enviar una sonda suave de RPM cada 1500ms para detectar cuando se vuelve a poner en contacto. + // No bombardear el bus OBD de la ECU continuamente (evita que parpadee la + // luz verde del scanner y drena bateria). Solo enviar una sonda suave de + // RPM cada 1500ms para detectar cuando se vuelve a poner en contacto. static uint32_t lastIgnitionProbeTime = 0; static uint32_t lastDimCheck = 0; if (millis() - lastDimCheck >= 500) { @@ -861,25 +1067,27 @@ void loop() { } } else { // Operacion normal: ciclo continuo y veloz de PIDs - prompt = false; //no prompt from ELM327 - pidIndex++; //point to next pid + prompt = false; // no prompt from ELM327 + pidIndex++; // point to next pid if (pidIndex >= maxpidIndex) { - pidIndex = 0; //back to pid 0 - autoDim(); //auto backlight handle + pidIndex = 0; // back to pid 0 + autoDim(); // auto backlight handle } - if (pidCurrentSkip[pidIndex] <= 0) { //end of skip loop , go next index - pidCurrentSkip[pidIndex] = pidReadSkip[pidIndex]; //read max delay loop to current + if (pidCurrentSkip[pidIndex] <= 0) { // end of skip loop , go next index + pidCurrentSkip[pidIndex] = + pidReadSkip[pidIndex]; // read max delay loop to current skip = false; - BTSerial.print(pidList[pidIndex] + "\r"); //send PID request + BTSerial.print(pidList[pidIndex] + "\r"); // send PID request lastPidSendTime = millis(); - } else { //skip sending request - pidCurrentSkip[pidIndex] = pidCurrentSkip[pidIndex] - 1; //decrese loop count + } else { // skip sending request + pidCurrentSkip[pidIndex] = + pidCurrentSkip[pidIndex] - 1; // decrese loop count prompt = true; skip = true; - } //else + } // else } - } //if prompt + } // if prompt /*------------------*/ -} // loop +} // loop diff --git a/sketch/obd2_gauge/touchscreen.h b/sketch/obd2_gauge/touchscreen.h index 3cf45a9..554c46b 100644 --- a/sketch/obd2_gauge/touchscreen.h +++ b/sketch/obd2_gauge/touchscreen.h @@ -4,28 +4,32 @@ #include "XPT2046_TouchscreenSOFTSPI.h" // Touchscreen pins for Cheap Yellow Display (CYD) -#define XPT2046_MOSI 32 // T_DIN -#define XPT2046_MISO 39 // T_OUT -#define XPT2046_CLK 25 // T_CLK -#define XPT2046_CS 33 // T_CS +#define XPT2046_MOSI 32 // T_DIN +#define XPT2046_MISO 39 // T_OUT +#define XPT2046_CLK 25 // T_CLK +#define XPT2046_CS 33 // T_CS extern SoftSPI *tsSoftSpi; extern XPT2046_TouchscreenSOFTSPI ts; #define TFT_GREY 0x5AEB #define _height 240 -#define _width 320 +#define _width 320 // 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++; diff --git a/sketch/obd2_gauge/tri_temp.h b/sketch/obd2_gauge/tri_temp.h index 5902505..4e3ff91 100644 --- a/sketch/obd2_gauge/tri_temp.h +++ b/sketch/obd2_gauge/tri_temp.h @@ -5,6 +5,7 @@ #include #include #include "DSEG7_Modern_BoldItalic_34.h" +#include "Latin_Sans_Bold_26.h" // Hardware 1-Wire Pin: Conector CN1 Pin 3 (GPIO 22) #define ONE_WIRE_BUS_PIN 22 @@ -33,12 +34,41 @@ 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(); + } + } + 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,6 +77,109 @@ 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, +}; + +static uint16_t s_unitRenderBuf[30 * 26]; + +// 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, uint16_t bgCol) { + for (int j = 0; j < 26; j++) { + int rowOffset = j * 30; + for (int i = 0; i < 30; i++) { + uint8_t a = pgm_read_byte(&s_dsegUnitAlpha[rowOffset + i]); + if (a == 0) { + s_unitRenderBuf[rowOffset + i] = bgCol; + } else if (a >= 250) { + s_unitRenderBuf[rowOffset + i] = color; + } else { + s_unitRenderBuf[rowOffset + i] = fastBlend(a, color, bgCol); + } + } + } + bool oldSwap = dst.getSwapBytes(); + dst.setSwapBytes(true); + dst.pushImage(bx, by, 30, 26, s_unitRenderBuf); + dst.setSwapBytes(oldSwap); +} + +// 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) { + sprBar.fillRoundRect(0, 0, fillW, 8, 3, accentCol); + } + sprBar.pushSprite(barX, barY); + } else { + tft.fillRoundRect(barX, barY, 120, 8, 3, 0x18C3); + if (fillW > 0) { + 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 @@ -71,7 +204,7 @@ 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; @@ -80,14 +213,14 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { else if (val < 110.0f) 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 } 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 @@ -96,26 +229,40 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { // Left vertical accent bar tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol); - // Titles - tft.setTextColor(TFT_WHITE, cardBg); - tft.drawString(titleStr, cardX + 16, cardY + 8, 2); + // Titles (Smooth anti-aliased font) + if (!sprTriTitleReady) initTriTempTitles(); + if (sprTriTitleReady && sprTriTitle[cardIdx].created()) { + sprTriTitle[cardIdx].pushSprite(cardX + 16, cardY + 6); + } else { + tft.loadFont(Latin_Sans_Bold_26); + tft.setTextPadding(0); + tft.setTextColor(TFT_WHITE, cardBg); + tft.drawString(titleStr, cardX + 16, cardY + 6, 1); + tft.unloadFont(); + } tft.setTextColor(0x8410, cardBg); // Soft gray - tft.drawString(subStr, cardX + 16, cardY + 28, 1); + tft.drawString(subStr, cardX + 16, cardY + 34, 1); // 3. Horizontal Progress Gauge Bar int barX = cardX + 16; int barY = cardY + 50; int barW = 120; - int barH = 8; - tft.fillRoundRect(barX, barY, barW, barH, 3, 0x18C3); // Trough background float ratio = (val - minRange) / (maxRange - minRange); if (ratio < 0.0f) ratio = 0.0f; if (ratio > 1.0f) ratio = 1.0f; int fillW = (int)(barW * ratio); if (fillW < 4) fillW = 4; - 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; @@ -129,43 +276,163 @@ inline void drawTriTempCard(uint8_t cardIdx, float val, int x_offset = 0) { 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.drawRightString(numStr.c_str(), cardX + 270, cardY + 20, 1); tft.unloadFont(); - // Unit "°C" - tft.setTextColor(0xC618, cardBg); - tft.drawString("`C", cardX + 268, cardY + 32, 2); + // Unit "°C" in authentic 7-segment digital italic matching number typography and aligned to digit base + if (x_offset == 0) { + draw7SegDegreeC(tft, cardX + 276, cardY + 28, accentCol, cardBg); + } else { + tft.setTextColor(accentCol, cardBg); + tft.drawString("`C", cardX + 276, cardY + 37, 2); + } +} + +// 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 = 6 + cardIdx * 77; // y = 6, 83, 160 + int cardH = 73; + 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; + if (val < 100.0f) accentCol = getThemePrimaryColor(); + else if (val < 110.0f) accentCol = TFT_YELLOW; + else accentCol = TFT_RED; + + if (oldVal < 100.0f) oldAccentCol = getThemePrimaryColor(); + else if (oldVal < 110.0f) 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 + 34, 130, 10, cardBg); + tft.setTextColor(0x8410, cardBg); + tft.drawString(subStr, cardX + 16, cardY + 34, 1); + } + } else if (cardIdx == 1) { + minRange = 10.0f; + maxRange = 40.0f; + accentCol = 0x07F3; // Mint Green + oldAccentCol = accentCol; + } else { + minRange = -10.0f; + maxRange = 50.0f; + accentCol = 0x3D7F; // Sky Blue + oldAccentCol = accentCol; + } + + // 1. Accent color changed: update vertical accent bar and unit "°C" + if (accentCol != oldAccentCol) { + tft.fillRoundRect(cardX + 6, cardY + 8, 4, cardH - 16, 2, accentCol); + draw7SegDegreeC(tft, cardX + 276, cardY + 28, accentCol, cardBg); + } + + // 2. Horizontal Progress Gauge Bar update (only redraw when pixel fill changes or accent changes) + int barX = cardX + 16; + int barY = cardY + 50; + 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); + } + + tft.loadFont(DSEG7_Modern_BoldItalic_34); + tft.setTextColor(accentCol, cardBg); + tft.setTextPadding(114); + tft.drawRightString(numStr.c_str(), cardX + 270, cardY + 20, 1); + tft.setTextPadding(0); + tft.unloadFont(); +} + +// 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] = { getThemePrimaryColor(), 0x07F3, 0x3D7F }; + 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) { + if (vals[i] >= 110.0f) acc = TFT_RED; + else if (vals[i] >= 100.0f) acc = TFT_YELLOW; + } + oldTriAccentCol[i] = acc; + } } // Draw full Tri-Temp screen inline void drawTriTempScreen(int x_offset = 0) { - if (x_offset == 0) { - tft.fillScreen(TFT_BLACK); - } else { + if (x_offset != 0) { tft.fillRect(0, 0, x_offset, 240, TFT_BLACK); + } else { + tft.fillRect(0, 0, 320, 6, TFT_BLACK); + tft.fillRect(0, 233, 320, 7, TFT_BLACK); } drawTriTempCard(0, tempEngineCoolant, x_offset); 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,13 +476,22 @@ 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); - oldTriInterior = tempInterior; - } - if (forceRedraw || abs(tempExterior - oldTriExterior) >= 0.1f) { 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) { + updateTriTempCardValue(2, tempExterior, oldTriExterior); + oldTriExterior = tempExterior; + } } } }