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:
parent
0315854a12
commit
d4f889b901
10 changed files with 4433 additions and 1706 deletions
23
LICENSE.md
23
LICENSE.md
|
|
@ -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
132
README.md
|
|
@ -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)
|
||||
---
|
||||
|
||||
-------------------------------------------------------
|
||||
[](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.*
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,78 +20,98 @@ 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
|
||||
phase = t % cycle_period
|
||||
if not self.auto_simulate:
|
||||
return
|
||||
|
||||
if phase < 5.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:
|
||||
# Batería en reposo / baja carga (12.0 - 13.2V) -> AMARILLO
|
||||
self.pcm_volt = 12.4
|
||||
self.rpm = 800.0
|
||||
self.speed = 0.0
|
||||
self.coolant_temp = 88.0
|
||||
elif phase < 16.0:
|
||||
# Marcha normal -> VERDE
|
||||
p = (phase - 9.0) / 7.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:
|
||||
# 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:
|
||||
# MÁXIMO ABSOLUTO (100% de escala) -> ROJO con alarma destellante
|
||||
self.pcm_volt = 16.0
|
||||
self.rpm = 6000.0
|
||||
self.speed = 160.0
|
||||
self.coolant_temp = 120.0
|
||||
else:
|
||||
# Retorno progresivo
|
||||
p = (phase - 27.0) / 5.0
|
||||
self.pcm_volt = 16.0 - p * (16.0 - 10.8)
|
||||
self.rpm = 6000.0 - p * (6000.0 - 800.0)
|
||||
self.speed = 160.0 - p * 160.0
|
||||
self.coolant_temp = 120.0 - p * (120.0 - 86.0)
|
||||
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
|
||||
|
||||
# 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)
|
||||
# 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 < 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
|
||||
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 = 850.0
|
||||
self.speed = 0.0
|
||||
self.coolant_temp = 88.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 = 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 - 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 - 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.oil_temp = self.coolant_temp + 3.0
|
||||
self.trans_temp = 82.0 + 1.0 * math.cos(t * 0.09)
|
||||
|
||||
state = VehicleState()
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 {
|
||||
tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
tft.drawString(text.c_str(), 6, yPos, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
line[i] = texts;
|
||||
texts = " " + texts;
|
||||
}
|
||||
tft.drawString(line[i], x, i * 20 + y, 2);
|
||||
} //for
|
||||
bt_message = ""; //reset bt message
|
||||
}
|
||||
|
||||
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!
|
||||
longPressHandled = true;
|
||||
clickCount = 0;
|
||||
casioBeep(4096, 30); // High confirmation chirp
|
||||
configMenu();
|
||||
if (foundOBD2) initScreen();
|
||||
isTouching = false;
|
||||
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;
|
||||
touchBeep(); // Confirmation chirp
|
||||
configMenu();
|
||||
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 (fullScreenCell == -1) {
|
||||
fullScreenCell = quad;
|
||||
} else {
|
||||
fullScreenCell = -1;
|
||||
}
|
||||
initScreen();
|
||||
} else {
|
||||
clickCount = 1;
|
||||
firstClickTime = millis();
|
||||
clickQuadrant = quad;
|
||||
}
|
||||
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();
|
||||
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 {
|
||||
BTSerial.disconnect();
|
||||
setRearLedOff();
|
||||
foundOBD2 = false;
|
||||
}
|
||||
} else {
|
||||
arcMeter4(clickQuadrant, pid, false);
|
||||
setRearLedOff();
|
||||
foundOBD2 = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
//----------------------
|
||||
//BLUETOOTH read
|
||||
#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,25 +789,57 @@ 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
|
||||
}
|
||||
pidIndex++; //point to next pid
|
||||
if (pidIndex >= maxpidIndex) {
|
||||
pidIndex = 0; //back to pid 0
|
||||
autoDim(); //auto backlight handle
|
||||
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
|
||||
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
|
||||
skip = false;
|
||||
BTSerial.print(pidList[pidIndex] + "\r"); //send PID request
|
||||
lastPidSendTime = millis();
|
||||
} else { //skip sending request
|
||||
pidCurrentSkip[pidIndex] = pidCurrentSkip[pidIndex] - 1; //decrese loop count
|
||||
prompt = true;
|
||||
skip = true;
|
||||
} //else
|
||||
}
|
||||
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
|
||||
lastPidSendTime = millis();
|
||||
} else { //skip sending request
|
||||
pidCurrentSkip[pidIndex] = pidCurrentSkip[pidIndex] - 1; //decrese loop count
|
||||
prompt = true;
|
||||
skip = true;
|
||||
} //else
|
||||
|
||||
} //if prompt
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue