feat: cluster digital Ford Ka Endura 1.3 con badge táctico ECO, alarmas sincronizadas y emulador OBD2

- Agregado badge táctico [ ECO ] en tiempo real (1900-2550 RPM, 45-82% carga motor)
- Sincronización de alarma visual destellante con titileo del título en pantalla completa
- Reajuste de escala y umbrales de temperatura (120°C fondo de escala, 110°C alarma)
- Configuración de emulador Bluetooth con modo fijo --eco y ciclo dinámico
- Limpieza de carpetas y archivos obsoletos del proyecto original
- Actualización completa de documentación en README.md
This commit is contained in:
Sebastián Nieto 2026-09-13 21:01:52 -03:00
parent 0315854a12
commit d4f889b901
10 changed files with 4433 additions and 1706 deletions

View file

@ -1,23 +0,0 @@
MIT No Commercial Use License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Non-Commercial Use Restriction:
The Software and any derivative works thereof must not be used, distributed,
or otherwise exploited for commercial purposes or for any financial benefit.

132
README.md
View file

@ -1,61 +1,99 @@
# OBD2 Gauge (ESP32 Bluetooth)
# Cluster Digital OBD-II ESP32 CYD (Ford Ka Endura 1.3)
## This repository has been archived and is no longer maintained. (October 24th, 2025)
Instrumento digital automotriz de alta precisión basado en la placa **ESP32 CYD (ESP32-2432S028)** con pantalla táctil resistiva TFT de 2.8" (320x240). Se comunica de forma inalámbrica vía Bluetooth SPP con un escáner **ELM327 v1.5** conectado al puerto OBD-II del vehículo.
A fully functional code-updated
Last Update: September 27th, 2024
El sistema fue diseñado y calibrado a medida para el motor **Ford Ka Endura-E 1.3** (y es compatible con instalaciones de GNC).
This is an ESP32 OBD2 Gauge for vehicles that are compatible with OBD2
(Special made for FORD vehicle)
- Prototype https://youtu.be/PkQaUJbzTNM
- Current Model Short Demo https://youtu.be/vvBIeim7XTE
---
## Características Principales
## Software:
- Arduino IDE 2.3.2 + ESP32 arduino core 2.0.17
- * IMPORTANT * this project won't work with ESP32 arduiino core 3.x.x
- modified TFT_eSPI library 2.5.43
### 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.
* **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.
## Hardware
- ESP32 TFT2.8 320x240 with "RESISTIVE" touch board.
buy here https://s.click.aliexpress.com/e/_Ew95gMl
- ELM327 Bluetooth adaptor (recommended v1.5 not v2.1)
Highly recommend ELM327 Bluetooth adaptor https://s.click.aliexpress.com/e/_oo3THvG
- A small speaker for mobile phone 1 Watt 8 ohm 9x28 mmm.
- A push button 6x6x5 2 legs
- 3d print enclosure
- 12v to 5v dc micro usb power regulator module
- Gauge magnetic stand
### 2. Modo Pantalla Completa (Full Screen)
* Tocando cualquier cuadrante, la pantalla se expande a vista completa para ese parámetro.
* Escala curvada de 20 bloques con **antialiasing de subpíxel** para bordes ultra suaves sin efecto serrucho.
* Tipografía digital retrofuturista **DSEG7 Modern 80pt** con coma decimal integrada y actualización veloz sin parpadeo (*flicker-free*).
* Alarma visual sincronizada: el icono, el arco y el título parpadean en rojo de forma simultánea ante sobretemperatura o sobretensión.
## Features:
- Show vehicle data 7 pids
* MAP - manifold air pressure (PSI)
* PCM - pcm voltage (volt)
* ENG LOAD - engine load (%)
* ENG SPD - engine speed (RPM)
* Coolant - coolant temperature (c)
* Oil Temp - engine oil temperature (c)
* TFT - Ford transmission fluid temperature (c)
### 3. Badge Táctico de Máxima Eficiencia `[ ECO ]`
* Calcula en tiempo real el punto óptimo de funcionamiento y entrega de torque (Sweet Spot) para el Ford Ka Endura 1.3:
* **RPM:** Entre 1.900 y 2.550 RPM (pico de par motor y mejor rendimiento volumétrico).
* **Carga de Motor (PID 0104 en segundo plano):** Entre 45% y 82% (apertura de mariposa con mínimas pérdidas por bombeo, antes del enriquecimiento a plena carga).
* **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.
- 8 layout page selectable display
- DTC read and clear function (Engine warning light only)
- Warning when parameter reaches setting value
- Adjustable warning value for each PIDs
- Configurable CPU overheat protection
- Configurable gauge Auto turn on/off
- Auto screen brightness
- Change off screen to user screen with micro SDcard
- Firmware updatable (micro SDcard and WiFi)
- VIN Read on the About page.
### 4. Audio y Melodías de Alarma
* Control de volumen independiente: Silencio, Ultra Bajo, Bajo (Casio suave), Medio y Alto.
* Selector de sonidos y tonos de alarma:
* Chime BMW (doble tono sincronizado).
* Casio Digital (doble chirp suave no invasivo).
* Tono bitonal clásico.
* Alarma continua urgente.
### 5. Menú de Configuración Táctil
* **Gestión de Alarmas:** Conmutadores táctiles independientes ON/OFF para cada parámetro con guardado persistente en NVS (Preferences).
* **Temas Visuales:** 6 paletas de color con degradados ambientales de fondo (*Cyan, Emerald, Amber, Violet, Ice Blue, Graphite*).
* **Brillo Automático:** Regulación inteligente mediante sensor de luz ambiente (LDR) integrado en la placa, o selección manual (5% a 100%).
* **Auto-Sleep:** Apagado automático de pantalla y suspensión ligera al detener el motor (RPM <= 300) tras el tiempo configurado (30s, 1m, 2m, 5m u OFF).
## [Flash Firmware Online](https://vaandcob.github.io/webpage/src/index.html)
---
-------------------------------------------------------
[![Buy Me a Coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=☕&slug=vaandcob&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff)](https://www.buymeacoffee.com/vaandcob)
## Hardware Requerido
1. **Placa ESP32 CYD 2.8" (ESP32-2432S028):**
* Pantalla TFT 320x240 con controlador ILI9341 / ST7789.
* Pantalla táctil resistiva con controlador XPT2046.
* Sensor de luz LDR en pin analógico.
* LED RGB integrado.
2. **Escáner OBD-II Bluetooth:**
* Adaptador ELM327 v1.5 (muy recomendado con chip PIC18F25K80, evitar clones v2.1 de baja calidad).
3. **Buzzer / Parlante:**
* Parlantito o buzzer pasivo de 8 ohms conectado al pin de audio del CYD (controlado por canal PWM con modulación de volumen).
4. **Alimentación:**
* Módulo convertidor 12V a 5V USB (conectado a positivo bajo llave o directo si se utiliza auto-sleep).
---
## Emulador OBD2 / ELM327 Bluetooth incluido
En la carpeta se incluye el script para emular un adaptador ELM327 v1.5 y la ECU del vehículo vía Bluetooth SPP en Linux. Permite probar y validar la pantalla CYD en banco de pruebas sin necesidad de conectarse a un auto real.
Consulta [emulator/README.md](emulator/README.md) para ver requisitos y modo de uso.
## Compilación y Carga de Firmware
El proyecto se encuentra preparado para compilarse tanto con `arduino-cli` como con Arduino IDE:
### Requisitos de Software
* **ESP32 Arduino Core:** Versión 2.0.17 (las ramas 3.x no son compatibles con el esquema de partición).
* **Librería TFT_eSPI:** 2.5.43 o superior con la configuración de pines del CYD.
* **Esquema de Partición:** `huge_app` (3MB APP / No OTA / LittleFS).
### Compilación desde Terminal
```bash
arduino-cli compile -b esp32:esp32:esp32:PartitionScheme=huge_app sketch/obd2_gauge
```
### Carga al Dispositivo
```bash
arduino-cli upload -p /dev/ttyUSB0 --fqbn esp32:esp32:esp32:PartitionScheme=huge_app sketch/obd2_gauge
```
---
## Emulador de Pruebas Bluetooth en Banco
En la carpeta `emulator/` se incluye el script `obd2_emulator.py` para simular un adaptador ELM327 v1.5 y la ECU del auto en Linux vía Bluetooth SPP:
### Modo ECO Fijo (Recomendado para calibración visual)
Mantiene los parámetros fijados en el punto dulce (2.250 RPM, 58% carga, 14.2V, 90°C) con el badge `[ ECO ]` permanentemente encendido:
```bash
python3 emulator/obd2_emulator.py --eco
```
### Modo Ciclo de Demostración
Ejecuta un ciclo dinámico de 36 segundos que incluye batería baja, ralentí, 10 segundos continuos en crucero ECO, aceleración exigida y fondo de escala con alarmas:
```bash
python3 emulator/obd2_emulator.py
```
*Nota: Mientras el emulador corre en la terminal, podés presionar `e` + Enter para activar el modo ECO fijo o `c` + Enter para volver al ciclo dinámico.*

View file

@ -20,8 +20,13 @@ Script en Python que emula un adaptador OBD-II Bluetooth ELM327 v1.5 completo y
2. Ejecuta el emulador:
```bash
# Modo normal (ciclo dinámico con fase ECO de 10s):
python3 emulator/obd2_emulator.py
# O modo ECO fijo permanente (RPM 2250, Carga 58%):
python3 emulator/obd2_emulator.py --eco
```
*Nota: En la terminal podés presionar 'e' + Enter para activar ECO fijo o 'c' + Enter para volver al ciclo.*
3. Enciende la pantalla ESP32 CYD. Se conectará automáticamente por Bluetooth SPP (Canal 1 RFCOMM).
4. El emulador responderá a los PIDs estándar de OBD2:

View file

@ -11,6 +11,7 @@ import math
import select
import socket
import threading
import argparse
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
@ -19,76 +20,96 @@ from gi.repository import GLib
# Vehicle Simulation State
class VehicleState:
def __init__(self):
self.rpm = 1800.0 # RPM
self.rpm = 2250.0 # RPM (Sweet spot default)
self.target_rpm = 2200.0 # Target RPM for smooth transitions
self.speed = 45.0 # km/h
self.coolant_temp = 88.0 # °C
self.oil_temp = 92.0 # °C
self.speed = 70.0 # km/h
self.coolant_temp = 90.0 # °C
self.oil_temp = 93.0 # °C
self.trans_temp = 82.0 # °C (Ford T6 221E1C)
self.load = 32.0 # % (0-100)
self.load = 58.0 # % (Sweet spot: 45% - 82%)
self.map_psi = 14.7 # psi (Manifold Absolute Pressure)
self.pcm_volt = 14.2 # Volts (MUST be > 12.5V or gauge will sleep)
self.auto_simulate = True
self.mode = "cycle" # "cycle" or "eco" (modo fijo)
self.lock = threading.Lock()
def update_simulation(self):
"""Simulates driving telemetry with clear transitions through color zones and holds at max scale"""
t = time.time()
with self.lock:
if self.auto_simulate:
# 32-second demonstration cycle:
# 0 - 5s: FASE 1 - Batería Baja Crítica (<12V) -> ROJO con alarma destellante (10.8V, 3 bloques)
# 5 - 9s: FASE 2 - Batería en reposo / baja carga (12.0 - 13.2V) -> AMARILLO (12.4V, 8 bloques)
# 9 - 16s: FASE 3 - Marcha normal / óptima -> Todo en VERDE (RPM 2200, Vel 60, Temp 92°C, Volt 14.2V)
# 16 - 21s: FASE 4 - Precaución / carga alta -> Todo en AMARILLO (RPM 4200, Vel 120, Temp 102°C, Volt 15.0V)
# 21 - 27s: FASE 5 - MÁXIMO ABSOLUTO (100% de escala) -> Todo en ROJO con alarma destellante (RPM 6000, Vel 160, Temp 120°C, Volt 16.0V)
# 27 - 32s: FASE 6 - Descenso suave de vuelta al inicio
cycle_period = 32.0
if not self.auto_simulate:
return
if self.mode == "eco":
# MODO ECO FIJO: Bloquea telemetría permanentemente en el punto dulce óptimo
self.pcm_volt = 14.2
self.rpm = 2250.0 # Rango ECO: 1900 - 2550 RPM
self.load = 58.0 # Rango ECO: 45% - 82%
self.speed = 70.0 # km/h
self.coolant_temp = 90.0 # °C
self.oil_temp = 93.0
self.map_psi = 14.5
self.trans_temp = 82.0
return
# Ciclo de demostración de 36 segundos con FASE ECO dedicada y prolongada:
# 0 - 4s: FASE 1 - Batería Baja Crítica (<12V) -> ROJO con alarma destellante (10.8V)
# 4 - 8s: FASE 2 - Ralentí y reposo (12.4V, 850 RPM, Carga 28%)
# 8 - 18s: FASE 3 - CRUCERO ÓPTIMO [ ECO ] (10 seg continuos: 2200 RPM, Carga 58%, 70 km/h, 14.2V) -> BADGE ACTIVO
# 18 - 24s: FASE 4 - Exigido / autopista (3800 - 4800 RPM, Carga 88%, 120 km/h, 102°C) -> Sale de ECO
# 24 - 30s: FASE 5 - MÁXIMO ABSOLUTO (100% escala: 6000 RPM, 160 km/h, 120°C, 16.0V) -> Todo en ROJO con alarma
# 30 - 36s: FASE 6 - Descenso suave de vuelta al inicio
cycle_period = 36.0
phase = t % cycle_period
if phase < 5.0:
if phase < 4.0:
# Batería baja crítica (<12V) -> ROJO destellante
self.pcm_volt = 10.8
self.rpm = 800.0
self.speed = 0.0
self.coolant_temp = 86.0
elif phase < 9.0:
self.load = 25.0
elif phase < 8.0:
# Batería en reposo / baja carga (12.0 - 13.2V) -> AMARILLO
self.pcm_volt = 12.4
self.rpm = 800.0
self.rpm = 850.0
self.speed = 0.0
self.coolant_temp = 88.0
elif phase < 16.0:
# Marcha normal -> VERDE
p = (phase - 9.0) / 7.0
self.load = 28.0
elif phase < 18.0:
# FASE ECO: Marcha y crucero óptimo durante 10 segundos continuos (RPM: 2150-2300, Carga: 56%-62%)
p = (phase - 8.0) / 10.0
self.pcm_volt = 14.2
self.rpm = 1500.0 + p * (2600.0 - 1500.0)
self.speed = 30.0 + p * (75.0 - 30.0)
self.coolant_temp = 90.0 + p * (94.0 - 90.0)
elif phase < 21.0:
self.rpm = 2200.0 + math.sin(p * math.pi) * 100.0
self.load = 58.0 + math.sin(p * math.pi) * 4.0
self.speed = 65.0 + p * 10.0
self.coolant_temp = 90.0 + p * 2.0
elif phase < 24.0:
# Exigido / autopista -> AMARILLO
p = (phase - 16.0) / 5.0
self.pcm_volt = 14.9 + p * 0.2 # 14.9 a 15.1V (Amarillo)
self.rpm = 3800.0 + p * (4800.0 - 3800.0) # 3800 a 4800 (Amarillo)
self.speed = 112.0 + p * (128.0 - 112.0) # 112 a 128 (Amarillo)
self.coolant_temp = 99.0 + p * (104.0 - 99.0) # 99 a 104 (Amarillo)
elif phase < 27.0:
p = (phase - 18.0) / 6.0
self.pcm_volt = 14.9 + p * 0.2
self.rpm = 3800.0 + p * (4800.0 - 3800.0)
self.load = 85.0 + p * 8.0
self.speed = 112.0 + p * (128.0 - 112.0)
self.coolant_temp = 99.0 + p * (104.0 - 99.0)
elif phase < 30.0:
# MÁXIMO ABSOLUTO (100% de escala) -> ROJO con alarma destellante
self.pcm_volt = 16.0
self.rpm = 6000.0
self.load = 96.0
self.speed = 160.0
self.coolant_temp = 120.0
else:
# Retorno progresivo
p = (phase - 27.0) / 5.0
p = (phase - 30.0) / 6.0
self.pcm_volt = 16.0 - p * (16.0 - 10.8)
self.rpm = 6000.0 - p * (6000.0 - 800.0)
self.load = 96.0 - p * (96.0 - 25.0)
self.speed = 160.0 - p * 160.0
self.coolant_temp = 120.0 - p * (120.0 - 86.0)
# Correlated auxiliary telemetry
self.map_psi = 10.0 + 16.0 * ((self.rpm - 800.0) / 5200.0)
self.load = 15.0 + 75.0 * ((self.rpm - 800.0) / 5200.0)
self.oil_temp = self.coolant_temp + 3.0
self.trans_temp = 82.0 + 1.0 * math.cos(t * 0.09)
@ -310,10 +331,18 @@ def handle_client_stream(sock):
sock.sendall(response.encode("latin1"))
now = time.time()
if now - last_display_time > 0.4:
if now - last_display_time > 0.25:
last_display_time = now
cmd_display = cmd if cmd else "<CR>"
print(f"\r[ECT: {state.coolant_temp:3.0f}°C | RPM: {state.rpm:4.0f} | SPEED: {state.speed:3.0f} km/h | Volt: {state.pcm_volt:4.1f}V | CMD: {cmd_display:6s} -> {response.strip()}]", end="", flush=True)
with state.lock:
c_rpm = state.rpm
c_load = state.load
c_temp = state.coolant_temp
c_spd = state.speed
c_volt = state.pcm_volt
is_eco = (1900.0 <= c_rpm <= 2550.0 and 45.0 <= c_load <= 82.0)
eco_badge = "\033[92;1m[ECO ACTIVO]\033[0m" if is_eco else "\033[90m[ECO OFF]\033[0m"
print(f"\r[RPM: {c_rpm:4.0f} | Carga: {c_load:2.0f}% {eco_badge} | {c_temp:3.0f}°C | {c_spd:3.0f} km/h | {c_volt:4.1f}V | CMD: {cmd_display:5s} -> {response.strip():14s}] ", end="", flush=True)
except socket.timeout:
pass
@ -338,9 +367,26 @@ def simulation_worker():
def run_emulator():
print("=" * 65)
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%%)")
args = parser.parse_args()
if args.eco or args.mode == "eco":
state.mode = "eco"
print("=" * 70)
print(" ESP32 CYD OBD2 Bluetooth Emulator (ELM327 v1.5)")
print("=" * 65)
print(" Soporte para Badge Táctico ECO / OPT (Ford Ka Endura 1.3)")
print("=" * 70)
if state.mode == "eco":
print(">>> MODO ACTUAL: ECO FIJO (RPM: 2250, Carga: 58%, Vel: 70 km/h) <<<")
else:
print(">>> MODO ACTUAL: CICLO DINÁMICO (Fase ECO activa entre 8s y 18s) <<<")
print("Comandos interactivos en consola:")
print(" - Escribe 'e' + [Enter]: Activar MODO ECO FIJO (permanente)")
print(" - Escribe 'c' + [Enter]: Activar MODO CICLO AUTOMÁTICO")
print(" - Presiona Ctrl+C o escribe 'q': Salir\n")
# Start simulation thread
sim_thread = threading.Thread(target=simulation_worker, daemon=True)
@ -423,12 +469,24 @@ def run_emulator():
try:
while True:
rlist, _, _ = select.select([server_sock], [], [], 1.0)
rlist, _, _ = select.select([server_sock, sys.stdin], [], [], 1.0)
if server_sock in rlist:
client_sock, client_info = server_sock.accept()
print(f"\n[Bluetooth Socket] Conexión entrante aceptada desde {client_info}!")
t = threading.Thread(target=handle_client_stream, args=(client_sock,), daemon=True)
t.start()
if sys.stdin in rlist:
line = sys.stdin.readline().strip().lower()
if line in ["e", "eco"]:
with state.lock:
state.mode = "eco"
print("\n\n>>> MODO ECO FIJO ACTIVADO: RPM=2250, Carga=58% (Badge [ ECO ] permanente) <<<\n")
elif line in ["c", "cycle"]:
with state.lock:
state.mode = "cycle"
print("\n\n>>> MODO CICLO AUTOMÁTICO ACTIVADO (Fase ECO activa entre 8s y 18s) <<<\n")
elif line in ["q", "quit", "exit"]:
break
except KeyboardInterrupt:
print("\nDeteniendo emulador...")
finally:

View file

@ -1,5 +1,11 @@
/* Bluetooth function */
//----------------------------------
extern bool ignitionOn;
extern bool wokeFromSleep;
void initScreen();
void plotNoData(uint8_t cell);
void touchBeep();
//convert bt address to text
//{0x00,0x1d,0xa5,0x00,0x12,0x92} -> 00:1d:a5:00:12:92
String ByteArraytoString(esp_bd_addr_t bt_address) {
@ -19,7 +25,7 @@ String ByteArraytoString(esp_bd_addr_t bt_address) {
/*=======================*/
void scanBTdevice() {//scan bluetooth device
digitalWrite(LED_BLUE_PIN,LOW);//blue led on
setRearLedColor(0, 0, 200);//blue led on
Serial.println("Scanning for OBDII Adaptor...");
Terminal("Scanning for OBDII Adaptor...",0,48,320,191);
btDeviceCount = 0;
@ -39,7 +45,7 @@ void scanBTdevice() {//scan bluetooth device
#ifdef SERIAL_DEBUG
Serial.println("Discovering stopped");
#endif
digitalWrite(LED_BLUE_PIN,HIGH);//blue led off
setRearLedOff();//blue led off
delay(1000);//redsicovery delay
} else {
@ -110,7 +116,8 @@ void scanBTdevice() {//scan bluetooth device
while(!BTSerial.connected(1000) && (try_count < 10)) {
Serial.print(F("."));
blink =! blink;
digitalWrite(LED_BLUE_PIN,blink);//blue led offset
if (blink) setRearLedColor(0, 0, 200);
else setRearLedOff();
if (blink) tft.setTextColor(TFT_BLUE,TFT_BLACK);
else tft.setTextColor(TFT_BLACK,TFT_BLACK);
tft.drawRightString("$",319,210,4);
@ -121,12 +128,13 @@ void scanBTdevice() {//scan bluetooth device
Terminal("Connect failed!",0,48,320,191);
Serial.println(F("Connect failed!"));
BTSerial.disconnect();
setRearLedOff();
foundOBD2 = false;
} else {
Terminal("Connected Successfully!",0,48,320,191);
Serial.println(F("Connected Successfully!"));
prompt = true;
digitalWrite(LED_GREEN_PIN, LOW);//green led
updateRearLedTheme(); // Set rear RGB LED to active theme color
foundOBD2 = true;
}
@ -139,7 +147,7 @@ void scanBTdevice() {//scan bluetooth device
//---------------------------
//connect to recent obdII for fast connection, skip scanning
void connectLastOBDII() {
digitalWrite(LED_BLUE_PIN,LOW);//blue led on
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);
@ -149,20 +157,120 @@ void connectLastOBDII() {
while(!BTSerial.connected(1000) && (try_count <3)) {
Serial.print(F("."));
blink =! blink;
digitalWrite(LED_BLUE_PIN,blink);//blue led offset
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;
digitalWrite(LED_GREEN_PIN, LOW);//green led
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) {
hasRecentAddr = true;
break;
}
}
if (!hasRecentAddr) {
foundOBD2 = false;
return false;
}
// Draw authentic themed resume card
tft.fillScreen(TFT_BLACK);
tft.fillRectVGradient(0, 0, 320, 26, getThemeAccentColor(), getThemeDarkColor());
tft.fillRect(0, 26, 320, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE);
tft.drawCentreString("Conexion Bluetooth", 160, 2, 4);
tft.fillRoundRect(14, 44, 292, 114, 6, getThemeDarkColor());
tft.drawRoundRect(14, 44, 292, 114, 6, getThemeAccentColor());
tft.fillRoundRect(18, 50, 4, 102, 2, getThemePrimaryColor());
tft.setTextColor(TFT_WHITE, getThemeDarkColor());
tft.drawCentreString("Reanudando conexion...", 160, 54, 4);
tft.setTextColor(0xBDD7, getThemeDarkColor());
tft.drawCentreString("Conectando con escaner OBDII", 160, 88, 2);
String macStr = ByteArraytoString(recent_client_addr);
tft.drawCentreString(macStr.c_str(), 160, 108, 2);
// Request Bluetooth connection to OBDII scanner
setRearLedColor(0, 0, 200); // Blue LED indicating connection attempt
BTSerial.disconnect();
delay(50);
BTSerial.connect(recent_client_addr, 0, sec_mask, role);
uint8_t try_count = 0;
bool blink = false;
while (!BTSerial.connected(1000) && (try_count < 4)) {
blink = !blink;
if (blink) setRearLedColor(0, 0, 200);
else setRearLedOff();
String dots = "";
for (uint8_t d = 0; d <= (try_count % 4); d++) dots += ". ";
tft.setTextColor(getThemePrimaryColor(), getThemeDarkColor());
tft.drawCentreString(dots.c_str(), 160, 132, 4);
uint16_t tx = 0, ty = 0;
if (getTouch(&tx, &ty) || digitalRead(SELECTOR_PIN) == LOW) {
break; // Abort on user interaction
}
try_count++;
}
if (BTSerial.connected()) {
tft.setTextColor(TFT_GREEN, getThemeDarkColor());
tft.drawCentreString("[ OK ] Conectado con exito!", 160, 132, 2);
setRearLedColor(0, 255, 0);
delay(200);
// Re-initialize ELM327 protocol and commands for Ford J1850 PWM
sendATCommand("ATE0", 800); // Echo OFF
sendATCommand("ATL0", 800); // Linefeeds OFF
sendATCommand("ATH0", 800); // Headers OFF
sendATCommand("ATAT1", 800); // Normal adaptive timing
sendATCommand("ATSP1", 800); // Ford J1850 PWM (Protocol 1)
String r0100 = sendATCommand("0100", 2500);
ignitionOn = (r0100.indexOf("41 00") != -1);
foundOBD2 = true;
prompt = true;
bt_message = "";
updateRearLedTheme(); // Restore theme color on rear RGB LED
touchBeep();
initScreen();
lastActivityTime = millis();
return true;
} else {
tft.setTextColor(TFT_RED, getThemeDarkColor());
tft.drawCentreString("[!] Escaner no disponible", 160, 132, 2);
BTSerial.disconnect();
setRearLedOff();
foundOBD2 = false;
delay(400);
initScreen();
for (uint8_t c = 0; c < 4; c++) {
plotNoData(c);
}
lastActivityTime = millis();
return false;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -49,9 +49,17 @@ Preferences pref; //create preference
#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
// GPIO 17 = BLUE LED
#define LED_RED_PIN 4
#define LED_GREEN_PIN 16
#define LED_BLUE_PIN 17
// PWM Channels 4, 5, 6 on Timers 2 & 3 (isolated from Timer 1 buzzer)
#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
@ -64,41 +72,126 @@ TFT_eSPI tft = TFT_eSPI();
#define TFT_GREY 0x5AEB
//ELM327 init https://www.elmelectronics.com/wp-content/uploads/2016/07/ELM327DS.pdf
const uint8_t elm327InitCount = 8;
const String elm327Init[elm327InitCount] = { "ATZ", "ATBRD23", "ATSP0", "ATAT2", "ATL0", "ATH0", "ATE0", "0100" }; //agressive wait response time from ECU
//const String elm327Init[elm327InitCount] = {"ATZ","ATPP0CSV35","ATPP0CON","ATSP0","ATAT2","ATL0","ATH0","ATE0","0100"};//agressive wait response time from ECU
/*------ PIDS total 7 now -------------------
Array data -> { Label , unit, pid, fomula, min, max, ,skip, digit, warn }
// ELM327 PIDs Configuration
/*------ PIDS total 4 -------------------
Array data -> { Label , unit, pid, fomula, min, max, skip, digit, warn }
label = Pid label show on meter
unit = unit for pid
pid = string pid 0104
formula = formula no to calculate
pid = string pid (e.g. 0105, 010C, 010D, ATRV)
formula = formula no to calculate (1: ECT, 3: RPM, 7: Speed, 8: ATRV)
min = lowest value
max = highest value
skip = delay reading 0 - 3; 3 = max delay read
digit = want to show digit or not
warn = default warning value
*/
const String pidConfig[4][9] = {
const String pidConfig[5][9] = {
//[pid][data]
{ "Temperatura", "`C", "0105", "1", "0", "120", "2", "0", "106" }, //0 = Temperatura (0105)
{ "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", "0142", "4", "10", "16", "1", "1", "15" }, //2 = Voltaje (0142)
{ "Velocidad", "km/h", "010D", "7", "0", "160", "0", "0", "130" } //3 = Velocidad (010D)
{ "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[4] = { "106", "5000", "15.0", "130" };
bool alarmEnabled[4] = { true, true, true, true };
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" };
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)
uint32_t lastActivityTime = 0; // Inactivity tracker for auto-sleep
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%" };
// Instrument Cluster & Menu Themes
struct GaugeTheme {
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
uint16_t darkColor; // Header gradient end / deep background
uint16_t bgColor; // Subtle desaturated ambient vignette background
uint8_t ledR; // Rear RGB LED Red (0..255)
uint8_t ledG; // Rear RGB LED Green (0..255)
uint8_t ledB; // Rear RGB LED Blue (0..255)
};
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
};
uint8_t themeIndex = 0; // Default 0: Azul Cian
inline uint16_t getThemePrimaryColor() {
if (themeIndex >= 6) return 0x07FF;
return GAUGE_THEMES[themeIndex].primaryColor;
}
inline uint16_t getThemeAccentColor() {
if (themeIndex >= 6) return 0x234F;
return GAUGE_THEMES[themeIndex].accentColor;
}
inline uint16_t getThemeDivColor() {
if (themeIndex >= 6) return 0x3412;
return GAUGE_THEMES[themeIndex].divColor;
}
inline uint16_t getThemeDarkColor() {
if (themeIndex >= 6) return 0x1105;
return GAUGE_THEMES[themeIndex].darkColor;
}
inline uint16_t getThemeBgColor() {
if (themeIndex >= 6) return 0x19C8;
return GAUGE_THEMES[themeIndex].bgColor;
}
// Control functions for the Rear RGB LED (Active-LOW, Common Anode)
void setRearLedColor(uint8_t r, uint8_t g, uint8_t b) {
// Ensure pins are attached to LEDC
ledcAttachPin(LED_RED_PIN, LED_RED_CH);
ledcAttachPin(LED_GREEN_PIN, LED_GREEN_CH);
ledcAttachPin(LED_BLUE_PIN, LED_BLUE_CH);
// Duty cycle for 8-bit (0..255): 255 = 3.3V (OFF), 0 = 0V (FULL ON)
ledcWrite(LED_RED_CH, 255 - r);
ledcWrite(LED_GREEN_CH, 255 - g);
ledcWrite(LED_BLUE_CH, 255 - b);
}
void setRearLedOff() {
ledcWrite(LED_RED_CH, 255);
ledcWrite(LED_GREEN_CH, 255);
ledcWrite(LED_BLUE_CH, 255);
// Drive pins directly HIGH as well for guaranteed zero leakage during sleep
digitalWrite(LED_RED_PIN, HIGH);
digitalWrite(LED_GREEN_PIN, HIGH);
digitalWrite(LED_BLUE_PIN, HIGH);
}
void updateRearLedTheme() {
if (themeIndex >= 6) return;
setRearLedColor(GAUGE_THEMES[themeIndex].ledR,
GAUGE_THEMES[themeIndex].ledG,
GAUGE_THEMES[themeIndex].ledB);
}
/*
0 - Refrigerante
@ -108,7 +201,6 @@ uint8_t alarmSoundType = 0; // Default 0: Casio Doble (0: Casio Doble, 1: Sin
*/
const uint8_t pidInCell[4] = { 0, 3, 1, 2 }; // 4 Cuadrantes: 0=Refrigerante, 1=Velocidad, 2=Revoluciones, 3=Voltaje
bool cellIsNumeric[4] = { false, false, false, false }; // false = Reloj (Arco), true = Solo Numero
int8_t fullScreenCell = -1; // -1 = 4 cuadrantes, 0..3 = pantalla completa de ese cuadrante
// User configuration here to change display >
@ -125,7 +217,9 @@ uint8_t B = 0; //get B
//Global variable
const uint32_t SLEEP_DURATION = 180 * 1000000; //180 sec* us = 3 min
String line[12] = { "", "", "", "", "", "", "", "", "", "", "", "" }; //buffer for each line in terminal function
const uint8_t MAX_TERMINAL_LINES = 13;
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
String serial_no = ""; //keep serial no.
@ -157,20 +251,88 @@ XPT2046_TouchscreenSOFTSPI ts(XPT2046_CS);
/*------------------*/
//TERMINAL terminal for debugging display text
void Terminal(String texts, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
uint8_t max_line = round((h - y) / 16.0);
tft.fillRect(x, y, x + w, y + h, TFT_BLACK);
tft.setTextColor(TFT_WHITE);
for (uint8_t i = 0; i < max_line; i++) {
if (i <= max_line - 2) {
line[i] = line[i + 1];
// Draw a single console boot entry with authentic status badges
void drawTerminalEntry(int lineIdx, const String &rawText) {
int yPos = 6 + lineIdx * 18;
String text = rawText;
// Clear that single line to prevent overlap if redrawn
tft.fillRect(0, yPos, 320, 18, TFT_BLACK);
if (text.startsWith("[ OK ]")) {
tft.setTextColor(0x7BEF, TFT_BLACK);
tft.drawString("[", 6, yPos, 2);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.drawString(" OK ", 12, yPos, 2);
tft.setTextColor(0x7BEF, TFT_BLACK);
tft.drawString("]", 52, yPos, 2);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString(text.substring(8).c_str(), 60, yPos, 2);
} else if (text.startsWith("[FAILED]")) {
tft.setTextColor(0x7BEF, TFT_BLACK);
tft.drawString("[", 6, yPos, 2);
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.drawString("FAILED", 12, yPos, 2);
tft.setTextColor(0x7BEF, TFT_BLACK);
tft.drawString("]", 52, yPos, 2);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString(text.substring(8).c_str(), 60, yPos, 2);
} else if (text.startsWith("[ INFO ]")) {
tft.setTextColor(0x7BEF, TFT_BLACK);
tft.drawString("[", 6, yPos, 2);
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.drawString(" INFO ", 12, yPos, 2);
tft.setTextColor(0x7BEF, TFT_BLACK);
tft.drawString("]", 52, yPos, 2);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString(text.substring(8).c_str(), 60, yPos, 2);
} else if (text.startsWith("[") && text.indexOf(']') != -1) {
int closeIdx = text.indexOf(']');
String tag = text.substring(0, closeIdx + 1);
String rest = text.substring(closeIdx + 1);
tft.setTextColor(0x07E0, TFT_BLACK); // Green kernel timestamp
tft.drawString(tag.c_str(), 6, yPos, 2);
int tagW = tft.textWidth(tag.c_str(), 2);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString(rest.c_str(), 6 + tagW + 4, yPos, 2);
} else {
line[i] = texts;
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawString(text.c_str(), 6, yPos, 2);
}
tft.drawString(line[i], x, i * 20 + y, 2);
} //for
bt_message = ""; //reset bt message
}
// 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) {
texts = "[FAILED] " + texts;
} else if (texts.indexOf("Scanning") != -1 || texts.indexOf("Iniciando") != -1 || texts.indexOf("Conectando") != -1 || texts.indexOf("Insert") != -1) {
texts = "[ INFO ] " + texts;
} else {
texts = " " + texts;
}
}
if (terminalLineCount < MAX_TERMINAL_LINES) {
line[terminalLineCount] = texts;
drawTerminalEntry(terminalLineCount, texts);
terminalLineCount++;
} else {
// Reached screen bottom: scroll up by 1 line
tft.fillScreen(TFT_BLACK);
for (uint8_t i = 0; i < MAX_TERMINAL_LINES - 1; i++) {
line[i] = line[i + 1];
drawTerminalEntry(i, line[i]);
}
line[MAX_TERMINAL_LINES - 1] = texts;
drawTerminalEntry(MAX_TERMINAL_LINES - 1, texts);
}
bt_message = "";
}
/*------------------*/
@ -185,10 +347,10 @@ void checkGenuine() { //check if genuine obd2 gauge - m
Serial.println(F("Not genuine Va&Cob Gauge!"));
while (true) { //beep
ledcWriteTone(buzzerChannel, 2000);
digitalWrite(LED_RED_PIN, LOW); //red on
setRearLedColor(255, 0, 0); //red on
delay(500);
ledcWriteTone(buzzerChannel, 0);
digitalWrite(LED_RED_PIN, HIGH); //red off
setRearLedOff(); //red off
delay(500);
}
} else {
@ -198,12 +360,80 @@ void checkGenuine() { //check if genuine obd2 gauge - m
}
//----------------------------------------------
// 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
BTSerial.print(cmd + "\r");
String resp = "";
uint32_t start = millis();
while (millis() - start < timeoutMs) {
if (BTSerial.available() > 0) {
char c = (char)BTSerial.read();
if (c == '>') {
break; // Prompt character received, command finished
}
resp += c;
} else {
delay(5);
}
}
resp.trim();
#ifdef SERIAL_DEBUG
Serial.printf("ELM CMD [%s] -> [%s]\n", cmd.c_str(), resp.c_str());
#endif
return resp;
}
//---- Include Header File --------------------
//#include "image.h"
#include "bluetooth.h"
#include "meter.h"
#include "config.h"
// Robust ELM327 and ECU connection routine
void initializeELM327() {
Terminal("[ INFO ] Iniciando comunicacion ELM327...");
// 1. Reset ELM327 (No ATBRD23! ATBRD breaks Bluetooth serial)
String rZ = sendATCommand("ATZ", 2500);
delay(300); // Allow ELM327 power and crystal stabilization
if (rZ.length() > 0) {
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)
// 3. Test Battery Voltage via ATRV (direct from OBD pin 16)
String rVolt = sendATCommand("ATRV", 1500);
if (rVolt.length() > 0) {
Terminal("[ OK ] Bateria: " + rVolt);
}
// 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)
String r0100 = sendATCommand("0100", 5000);
if (r0100.indexOf("41 00") != -1) {
Terminal("[ OK ] ECU OK: Ford J1850 PWM!");
ignitionOn = true;
delay(600);
} else {
Terminal("[ INFO ] Sin contacto (Poner en marcha)");
ignitionOn = false;
delay(1000);
}
initScreen();
beepbeep();
bt_message = "";
prompt = true;
}
//----SET UP -----------------------------------
void setup() {
//pin configuration
@ -211,12 +441,14 @@ void setup() {
pinMode(LDR_PIN, ANALOG); //ldr analog input read brightness
pinMode(BUZZER_PIN, OUTPUT); //speaker
pinMode(SELECTOR_PIN, INPUT_PULLUP); //button
pinMode(LED_RED_PIN, OUTPUT); //red
pinMode(LED_GREEN_PIN, OUTPUT); //green
pinMode(LED_BLUE_PIN, OUTPUT); //blue
digitalWrite(LED_RED_PIN, LOW); //on
digitalWrite(LED_GREEN_PIN, HIGH); //off
digitalWrite(LED_BLUE_PIN, HIGH); //off
// Rear RGB LED PWM setup (Channels 4, 5, 6 - Timers 2 & 3)
ledcSetup(LED_RED_CH, 5000, 8);
ledcSetup(LED_GREEN_CH, 5000, 8);
ledcSetup(LED_BLUE_CH, 5000, 8);
ledcAttachPin(LED_RED_PIN, LED_RED_CH);
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
@ -259,16 +491,22 @@ void setup() {
//backlight ledcAttachPin must be set after tft.init()
ledcAttachPin(TFT_BL, backlightChannel); //attach backlight
for (uint8_t i = 255; i > 0; i--) { //fading effect
ledcWrite(backlightChannel, i); //full bright
delay(5);
}
digitalWrite(LED_RED_PIN, HIGH); //red off
tft.fillScreen(TFT_BLACK); //show start page
tft.fillRectVGradient(0, 0, 320, 26, 0x2104, TFT_BLACK);
tft.setTextColor(TFT_WHITE);
tft.drawString("OBDII GAUGE", 10, 5, 2);
ledcWrite(backlightChannel, 255); //full bright
tft.fillScreen(TFT_BLACK); // Consola de arranque de sistema
terminalLineCount = 0;
Terminal("[ 0.000000] ESP32-D0WD Xtensa LX6 Dual-Core @ 240MHz");
delay(40);
Terminal("[ 0.012400] FreeRTOS Kernel v10.4.3 (ESP-IDF v4.4)");
delay(40);
Terminal("[ 0.025800] Flash: 4MB QIO @ 80MHz | huge_app");
delay(40);
Terminal("[ 0.041200] SRAM: 520KB | Core 0/1 Online");
delay(40);
Terminal("[ OK ] Display: ILI9341 320x240 LCD Initialized");
delay(40);
Terminal("[ OK ] Input: XPT2046 Resistive Touch Calibrated");
delay(40);
// checkGenuine();//check guniune
//init variable
@ -285,26 +523,49 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6]
pref.putUShort("layout", 0);
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] == "")) {
warningValue[0] = "110";
pref.putString(pidConfig[0][0].c_str(), "110");
}
String almKey = "alm_" + String(i);
alarmEnabled[i] = pref.getBool(almKey.c_str(), true);
alarmEnabled[i] = pref.getBool(almKey.c_str(), (i < 4));
}
buzzerVolumeLevel = pref.getUChar("b_vol", 3);
if (buzzerVolumeLevel > 5) buzzerVolumeLevel = 3;
beepSoundType = pref.getUChar("b_type", 0);
if (beepSoundType > 5) beepSoundType = 0;
alarmVolumeLevel = pref.getUChar("alm_vol", 3);
if (alarmVolumeLevel > 5) alarmVolumeLevel = 3;
alarmSoundType = pref.getUChar("alm_type", 0);
if (alarmSoundType > 3) alarmSoundType = 0;
if (alarmSoundType > 5) alarmSoundType = 0;
autoSleepIndex = pref.getUChar("auto_sleep", 2);
if (autoSleepIndex > 4) autoSleepIndex = 2;
brightnessIndex = pref.getUChar("b_bright", 0);
if (brightnessIndex > 5) brightnessIndex = 0;
themeIndex = pref.getUChar("theme_idx", 0);
if (themeIndex > 5) themeIndex = 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
updateRearLedTheme(); // Sync rear RGB LED with loaded theme
Terminal("[ OK ] Settings: Theme " + String(GAUGE_THEMES[themeIndex].name) + " (LED RGB)");
delay(40);
//start bluetooth
Terminal("[ INFO ] Initializing Bluetooth Controller...");
if (!BTSerial.begin("Va&Cob OBDII Gauge", true)) {
Serial.println(F("Bluetooth..error!"));
Terminal("Bluetooth..error!", 0, 48, 320, 191);
Terminal("[FAILED] Bluetooth controller initialization error!");
abort();
} else {
Serial.println(F("Bluetooth..OK"));
Terminal("Bluetooth..OK", 0, 48, 320, 191);
Terminal("[ OK ] Bluetooth SPP Master stack online");
}
#ifdef SKIP_CONNECTION
@ -321,38 +582,13 @@ recent_client_addr : {0x00,0x00,0x00,0x00,0x00,0x00} array of bytes[6]
delay(50);
if (digitalRead(SELECTOR_PIN) == LOW) {
configMenu();
waitForTouchRelease();
}
}
}
//Initialize ELM327
Terminal("Initializing...", 0, 48, 320, 191);
bt_message = "";
for (uint8_t initIndex = 0; initIndex < elm327InitCount; initIndex++) { //send init ELM327 AT command
BTSerial.print(elm327Init[initIndex] + "\r");
if (initIndex == elm327InitCount - 1) delay(2000); //wait for 0100 Searching..protocal
else delay(100); //must add delay , unless will skip BTSerial.avaialble>0
while (BTSerial.available() > 0) {
char incomingChar = BTSerial.read();
if (incomingChar == '>') { //check prompt
BTSerial.flush(); //clear buffer
#ifdef SERIAL_DEBUG
Serial.println(bt_message);
#endif
#ifdef TERMINAL
Terminal(bt_message, 0, 48, 320, 191);
#endif
if (initIndex == 1) BTSerial.print("\r"); //ATBRD23 response
bt_message = "";
} else {
bt_message.concat(incomingChar); //keep elm327 respond
}
} //while
} //for
initScreen();
beepbeep();
prompt = true;
initializeELM327();
//----------------------
} //setup
@ -361,103 +597,156 @@ void loop() {
updateBacklightFade(); // non-blocking smooth backlight fade handler
handleArcAlarmFlash(); // non-blocking arc alarm flash handler (blinks when in TFT_RED)
if (wokeFromSleep) {
wokeFromSleep = false;
lastActivityTime = millis();
resumeBluetoothConnection();
}
// SCAN TOUCHSCREEN & BUTTON
// - Single click on a quadrant: toggles between clock (arc) and number
// - Double click on a quadrant: puts that instrument in full screen (or exits full screen)
// - Single tap on a quadrant in 4-quadrant view: switches to full screen for that quadrant
// - Single tap anywhere in full screen view: returns to 4-quadrant view
// - Long press (> 1s) on screen OR holding selector button: opens configuration menu
uint16_t touchX = 0, touchY = 0;
bool isTouched = getTouch(&touchX, &touchY);
bool btnPressed = (digitalRead(SELECTOR_PIN) == LOW);
if (isTouched || btnPressed) {
lastActivityTime = millis();
}
static bool isTouching = false;
static uint32_t touchStartTime = 0;
static uint16_t lastTouchX = 0, lastTouchY = 0;
static uint8_t clickCount = 0;
static uint32_t firstClickTime = 0;
static int8_t clickQuadrant = -1;
static bool longPressHandled = false;
if (isTouched) {
if (btnPressed) {
delay(50);
if (digitalRead(SELECTOR_PIN) == LOW) {
touchBeep();
configMenu();
waitForTouchRelease();
lastActivityTime = millis();
initScreen();
}
} else if (isTouched) {
if (!isTouching) {
isTouching = true;
touchStartTime = millis();
lastTouchX = touchX;
lastTouchY = touchY;
longPressHandled = false;
casioBeep(4096, 25); // Crisp, bright Casio chirp on touch screen press!
} else if (!longPressHandled && (millis() - touchStartTime > 1000)) {
// Long press (> 1s): Open Config Menu!
touchBeep(); // Crisp acoustic feedback on touch screen press!
} else {
// Keep tracking coordinates while held to ensure stable quadrant detection
lastTouchX = touchX;
lastTouchY = touchY;
if (!longPressHandled && (millis() - touchStartTime > 1000)) {
// Long press (> 1s): Open Config Menu immediately!
longPressHandled = true;
clickCount = 0;
casioBeep(4096, 30); // High confirmation chirp
touchBeep(); // Confirmation chirp
configMenu();
if (foundOBD2) initScreen();
waitForTouchRelease();
lastActivityTime = millis();
initScreen();
isTouching = false;
}
}
} else {
// Touch / button released
if (isTouching) {
isTouching = false;
if (!longPressHandled && (millis() - touchStartTime >= 30)) {
// Valid tap detected! Determine quadrant
int quad = (lastTouchX < 160) ? ((lastTouchY < 120) ? 0 : 2) : ((lastTouchY < 120) ? 1 : 3);
if (clickCount == 0) {
clickCount = 1;
firstClickTime = millis();
clickQuadrant = quad;
} else if (clickCount == 1) {
if ((millis() - firstClickTime < 400) && (quad == clickQuadrant || fullScreenCell != -1)) {
// DOUBLE CLICK DETECTED!
clickCount = 0;
if (!longPressHandled) {
// Valid tap detected!
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;
}
initScreen();
} else {
clickCount = 1;
firstClickTime = millis();
clickQuadrant = quad;
}
}
waitForTouchRelease(); // Clear mechanical bounce on release
}
}
}
// Handle single click timeout (after 400ms without a second tap)
if (clickCount == 1 && (millis() - firstClickTime >= 400)) {
clickCount = 0;
// 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 && 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();
}
}
if (fullScreenCell != -1) {
// In full screen: single click toggles between arc and number mode without header flicker
cellIsNumeric[fullScreenCell] = !cellIsNumeric[fullScreenCell];
fullScreenMeter(fullScreenCell, pidInCell[fullScreenCell], false);
} else if (clickQuadrant >= 0 && clickQuadrant < 4) {
// In 4 quadrants: single click toggles between arc and number for this quadrant without header flicker
cellIsNumeric[clickQuadrant] = !cellIsNumeric[clickQuadrant];
uint8_t pid = pidInCell[clickQuadrant];
if (cellIsNumeric[clickQuadrant]) {
numericMeter4(clickQuadrant, pid, false);
#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;
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 {
arcMeter4(clickQuadrant, pid, false);
BTSerial.disconnect();
setRearLedOff();
foundOBD2 = false;
}
} else {
setRearLedOff();
foundOBD2 = false;
}
}
return;
}
#endif
//----------------------
//BLUETOOTH read
static uint32_t lastPidSendTime = 0;
// Watchdog: If waiting for prompt for more than 6000ms (standard OBD2 maximum ECU timeout), recover!
if (!prompt && (millis() - lastPidSendTime > 6000)) {
// 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());
#endif
BTSerial.flush();
bt_message = "";
prompt = true;
skip = true;
skip = true; // IMPORTANT: skip updateMeter so timeout never puts zeros on screen!
}
while (BTSerial.available() > 0) {
@ -500,10 +789,41 @@ void loop() {
//Send another PID to elm327
if (prompt) { //> ELM327 ready! -> request next PID
prompt = false; //no prompt from ELM327
if (!skip) {
if (!skip && bt_message.length() > 0) {
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.
static uint32_t lastIgnitionProbeTime = 0;
static uint32_t lastDimCheck = 0;
if (millis() - lastDimCheck >= 500) {
lastDimCheck = millis();
autoDim();
}
if (millis() - lastIgnitionProbeTime >= 1500) {
lastIgnitionProbeTime = millis();
prompt = false;
skip = false;
// Buscar el indice de RPM en pidList (dentro de los 4 cuadrantes)
uint8_t rpmPidNo = 2; // Default cell 2 = RPM
for (uint8_t i = 0; i < 4; i++) {
if (pidConfig[pidInCell[i]][3].toInt() == 3) {
rpmPidNo = i;
break;
}
}
pidIndex = rpmPidNo;
BTSerial.print(pidList[pidIndex] + "\r");
lastPidSendTime = millis();
}
} else {
// Operacion normal: ciclo continuo y veloz de PIDs
prompt = false; //no prompt from ELM327
pidIndex++; //point to next pid
if (pidIndex >= maxpidIndex) {
pidIndex = 0; //back to pid 0
@ -519,6 +839,7 @@ void loop() {
prompt = true;
skip = true;
} //else
}
} //if prompt

View file

@ -16,12 +16,6 @@ extern XPT2046_TouchscreenSOFTSPI ts;
#define _height 240
#define _width 320
// Direct touch check using pressure Z
inline bool isScreenPressed() {
if (!tsSoftSpi) return false;
return ts.touched(tsSoftSpi);
}
// Get mapped touchscreen coordinates in landscape (320x240)
bool getTouch(uint16_t *x, uint16_t *y) {
if (!tsSoftSpi) return false;
@ -29,8 +23,8 @@ bool getTouch(uint16_t *x, uint16_t *y) {
if (ts.touched(tsSoftSpi)) {
TS_Point p = ts.getPoint(tsSoftSpi);
// Filter noise: require solid touch pressure and coordinates within active screen
if (p.z < 600) return false;
// Filter noise: require valid touch pressure and coordinates within active screen
if (p.z < 350) 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);
@ -43,6 +37,27 @@ bool getTouch(uint16_t *x, uint16_t *y) {
return false;
}
// Direct touch check using pressure Z
inline bool isScreenPressed() {
if (!tsSoftSpi) return false;
return ts.touched(tsSoftSpi);
}
// Wait until both touchscreen and selector button are completely released with 50ms continuous debounce
inline void waitForTouchRelease() {
uint16_t dummyX, dummyY;
uint8_t releaseCount = 0;
while (releaseCount < 5) {
if ((tsSoftSpi && ts.touched(tsSoftSpi)) || getTouch(&dummyX, &dummyY) || (digitalRead(SELECTOR_PIN) == LOW)) {
releaseCount = 0;
} else {
releaseCount++;
}
delay(10);
yield();
}
}
void touch_calibrate() {
// Calibration placeholder for backward compatibility
}