fix CAN ERROR

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2026-01-15 16:58:42 +01:00
padre 5332809c40
commit 4192db42e2
Se han modificado 7 ficheros con 562 adiciones y 29 borrados

Ver fichero

@@ -2,6 +2,44 @@
A modern Android application for communicating with OBD2 ELM327 Bluetooth adapters to read vehicle diagnostics and send custom commands.
## 🆕 Mejoras Recientes - Solución para CAN ERROR
### Cambios en la Comunicación Bluetooth
#### 1. **Inicialización Mejorada del ELM327**
-**Tiempos de espera más largos**: Reset ahora espera 2 segundos (antes 1 segundo)
-**Espacios activados (ATS1)**: Mejor compatibilidad con más vehículos
-**Adaptive timing conservador (ATAT1)**: Más confiable que ATAT2
-**Test de comunicación automático**: Verifica conexión con el vehículo después de la inicialización
-**Logging detallado**: Cada paso de inicialización se registra con su respuesta
#### 2. **Formateo Automático de Comandos**
- Los comandos OBD ahora se formatean automáticamente con espacios
- Ejemplo: `010C``01 0C`
- Mejora la compatibilidad con diferentes adaptadores ELM327
#### 3. **Timeout Aumentado**
- Timeout de lectura: **10 segundos** (antes 5 segundos)
- Permite tiempo suficiente para vehículos que responden lentamente
#### 4. **Mejor Manejo de Errores**
- Detección mejorada de "CAN ERROR"
- Logging de bytes enviados en hexadecimal
- Información detallada de respuestas
### Nueva Pantalla: Ayuda de Diagnóstico 🔧
Agregamos una pantalla completa de ayuda para resolver problemas de conexión:
-**Verificaciones rápidas**: Lista de chequeo de problemas comunes
-**Comandos de diagnóstico paso a paso**: ATZ, ATI, ATDP, 0100, 010C
-**Pruebas de protocolos**: Botones para probar protocolos CAN específicos
-**Respuestas en tiempo real**: Ve la última respuesta del adaptador
-**Consejos específicos**: Explicaciones de errores comunes
#### Acceso a la Ayuda
Desde la pantalla principal, pulsa en **"🔧 Ayuda de Diagnóstico"**
## Features
### 🔌 Bluetooth Connection

Ver fichero

@@ -61,6 +61,9 @@ fun OBD2App() {
},
onNavigateToCustomCommand = {
navController.navigate(Screen.CustomCommand.route)
},
onNavigateToDiagnosticHelp = {
navController.navigate(Screen.DiagnosticHelp.route)
}
)
}
@@ -86,6 +89,13 @@ fun OBD2App() {
)
}
composable(Screen.DiagnosticHelp.route) {
DiagnosticHelpScreen(
viewModel = mainViewModel,
onNavigateBack = { navController.popBackStack() }
)
}
composable(Screen.CustomCommand.route) {
CustomCommandScreen(
viewModel = mainViewModel,

Ver fichero

@@ -88,35 +88,52 @@ class BluetoothService {
* Based on standard ELM327 initialization sequence
*/
private suspend fun initializeElm327() {
Log.d(TAG, "Starting ELM327 initialization...")
// Reset device
sendCommand("ATZ")
kotlinx.coroutines.delay(1000)
val resetResponse = sendCommand("ATZ")
Log.d(TAG, "ATZ Response: ${resetResponse.getOrNull()}")
kotlinx.coroutines.delay(2000) // Wait longer after reset
// Turn off echo
sendCommand("ATE0")
kotlinx.coroutines.delay(100)
val echoResponse = sendCommand("ATE0")
Log.d(TAG, "ATE0 Response: ${echoResponse.getOrNull()}")
kotlinx.coroutines.delay(200)
// Set line feed off
sendCommand("ATL0")
kotlinx.coroutines.delay(100)
val lineFeedResponse = sendCommand("ATL0")
Log.d(TAG, "ATL0 Response: ${lineFeedResponse.getOrNull()}")
kotlinx.coroutines.delay(200)
// Set spaces off (faster parsing)
sendCommand("ATS0")
kotlinx.coroutines.delay(100)
// Keep spaces ON (better compatibility with more vehicles)
val spacesResponse = sendCommand("ATS1")
Log.d(TAG, "ATS1 Response: ${spacesResponse.getOrNull()}")
kotlinx.coroutines.delay(200)
// Turn off headers (shorter responses)
sendCommand("ATH0")
kotlinx.coroutines.delay(100)
val headersResponse = sendCommand("ATH0")
Log.d(TAG, "ATH0 Response: ${headersResponse.getOrNull()}")
kotlinx.coroutines.delay(200)
// Set adaptive timing to mode 2 (aggressive learning for faster responses)
sendCommand("ATAT2")
kotlinx.coroutines.delay(100)
// Set adaptive timing to mode 1 (more conservative)
val timingResponse = sendCommand("ATAT1")
Log.d(TAG, "ATAT1 Response: ${timingResponse.getOrNull()}")
kotlinx.coroutines.delay(200)
// Set protocol to automatic
sendCommand("ATSP0")
kotlinx.coroutines.delay(100)
val protocolResponse = sendCommand("ATSP0")
Log.d(TAG, "ATSP0 Response: ${protocolResponse.getOrNull()}")
kotlinx.coroutines.delay(500)
Log.d(TAG, "ELM327 initialized successfully")
// Try to connect to vehicle with test command
val testResponse = sendCommand("0100")
Log.d(TAG, "0100 Test Response: ${testResponse.getOrNull()}")
if (testResponse.isSuccess) {
Log.d(TAG, "ELM327 initialized successfully and vehicle responding")
} else {
Log.w(TAG, "ELM327 initialized but vehicle may not be responding")
}
}
/**
@@ -128,10 +145,13 @@ class BluetoothService {
}
try {
Log.d(TAG, "Preparing to send command: '$command'")
// Format OBD commands properly (add space between mode and PID if needed)
val formattedCommand = formatObdCommand(command)
Log.d(TAG, "Preparing to send command: '$command' -> formatted: '$formattedCommand'")
Log.d(TAG, "Command type: ${command::class.java.simpleName}, length: ${command.length}")
val commandWithCR = "$command\r"
val commandWithCR = "$formattedCommand\r"
val bytes = commandWithCR.toByteArray()
Log.d(TAG, "Sending bytes: ${bytes.joinToString(" ") { "0x%02X".format(it) }}")
@@ -139,13 +159,19 @@ class BluetoothService {
outputStream?.write(bytes)
outputStream?.flush()
Log.d(TAG, "Sent: $command")
Log.d(TAG, "Sent: $formattedCommand")
// Read response
val response = readResponse()
_lastResponse.value = response
Log.d(TAG, "Received: $response")
// Check for errors
if (response.contains("CAN ERROR", ignoreCase = true)) {
Log.e(TAG, "CAN ERROR received - possible issues: wrong protocol, vehicle off, or unsupported command")
}
Result.success(response)
} catch (e: Exception) {
Log.e(TAG, "Send command error: ${e.message}", e)
@@ -153,6 +179,35 @@ class BluetoothService {
}
}
/**
* Format OBD command to ensure proper spacing
* OBD commands should be: MODE PID (e.g., "01 0C")
* AT commands stay as is
*/
private fun formatObdCommand(command: String): String {
val trimmed = command.trim().replace(" ", "")
// If it's an AT command, return as is
if (trimmed.startsWith("AT", ignoreCase = true)) {
return trimmed.uppercase()
}
// If it's a short command (like "03", "04"), return as is
if (trimmed.length <= 2) {
return trimmed.uppercase()
}
// For OBD commands (like "010C"), add space between mode and PID
// Format: XX YY or XX YYYY
if (trimmed.length == 4) {
return "${trimmed.substring(0, 2)} ${trimmed.substring(2, 4)}".uppercase()
} else if (trimmed.length == 6) {
return "${trimmed.substring(0, 2)} ${trimmed.substring(2, 6)}".uppercase()
}
return trimmed.uppercase()
}
/**
* Read response from ELM327
*/
@@ -163,7 +218,7 @@ class BluetoothService {
try {
val startTime = System.currentTimeMillis()
val timeout = 5000 // 5 seconds timeout
val timeout = 10000 // 10 seconds timeout (increased for slower vehicles)
while (System.currentTimeMillis() - startTime < timeout) {
if (inputStream?.available() ?: 0 > 0) {
@@ -171,24 +226,43 @@ class BluetoothService {
if (bytesRead > 0) {
val chunk = String(buffer, 0, bytesRead)
response.append(chunk)
Log.d(TAG, "Read chunk: ${chunk.replace("\r", "\\r").replace("\n", "\\n")}")
// Check for prompt character '>'
if (chunk.contains('>')) {
Log.d(TAG, "Found prompt character, stopping read")
break
}
}
} else {
// Check if we have any response and it looks complete
val currentResponse = response.toString()
if (currentResponse.isNotEmpty() &&
(currentResponse.contains("OK") ||
currentResponse.contains("ERROR") ||
currentResponse.contains("NO DATA"))) {
Log.d(TAG, "Response looks complete without prompt")
break
}
}
Thread.sleep(10)
Thread.sleep(50) // Increased sleep time
}
if (System.currentTimeMillis() - startTime >= timeout) {
Log.w(TAG, "Response timeout reached")
}
} catch (e: Exception) {
Log.e(TAG, "Read response error", e)
}
return response.toString()
val cleanedResponse = response.toString()
.replace(">", "")
.replace("\r", "")
.replace("\n", "")
.replace("\n", " ")
.trim()
Log.d(TAG, "Final cleaned response: '$cleanedResponse'")
return cleanedResponse
}
/**

Ver fichero

@@ -98,14 +98,22 @@ object ObdCommands {
* Parse OBD2 response for common PIDs
*/
fun parseResponse(command: String, response: String): String? {
if (response.contains("NO DATA") || response.contains("ERROR")) {
// Check for various error conditions
if (response.contains("NO DATA", ignoreCase = true) ||
response.contains("ERROR", ignoreCase = true) ||
response.contains("UNABLE", ignoreCase = true) ||
response.contains("?", ignoreCase = true) ||
response.isBlank()) {
return null
}
// Remove spaces and common prefixes
val cleanResponse = response.replace(" ", "").replace("41", "").replace("43", "")
// Remove spaces and common prefixes (41 is response to mode 01, 43 is response to mode 03)
val cleanResponse = response.replace(" ", "").replace("41", "").replace("43", "").uppercase()
return when (command) {
// Normalize command for comparison
val cleanCommand = command.replace(" ", "").uppercase()
return when (cleanCommand) {
"010C" -> { // RPM
try {
val value = cleanResponse.substring(2, 6).toInt(16)

Ver fichero

@@ -8,6 +8,7 @@ sealed class Screen(val route: String) {
object Bluetooth : Screen("bluetooth")
object VehicleSelection : Screen("vehicle_selection")
object Diagnostic : Screen("diagnostic")
object DiagnosticHelp : Screen("diagnostic_help")
object CustomCommand : Screen("custom_command")
object About : Screen("about")
}

Ver fichero

@@ -0,0 +1,392 @@
package com.manalejandro.odb2bluetooth.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.manalejandro.odb2bluetooth.ui.viewmodel.MainViewModel
/**
* Diagnostic help screen with troubleshooting steps
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DiagnosticHelpScreen(
viewModel: MainViewModel,
onNavigateBack: () -> Unit
) {
val isLoading by viewModel.isLoading.collectAsState()
val commandHistory by viewModel.commandHistory.collectAsState()
val lastResponse = commandHistory.firstOrNull()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Diagnóstico de Conexión") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver")
}
}
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Error CAN ERROR explanation
item {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.width(8.dp))
Text(
"¿Qué es CAN ERROR?",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
Text(
"El error 'CAN ERROR' significa que el adaptador ELM327 no puede comunicarse con la ECU del vehículo.",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
// Quick checks
item {
Text(
"Verificaciones Rápidas",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
item {
ChecklistCard(
title = "1. Vehículo encendido",
description = "Asegúrate de que la llave está en posición 'ON' o el motor está encendido",
icon = Icons.Default.CheckCircle
)
}
item {
ChecklistCard(
title = "2. Adaptador conectado",
description = "Verifica que el ELM327 está bien conectado al puerto OBD2 del vehículo",
icon = Icons.Default.CheckCircle
)
}
item {
ChecklistCard(
title = "3. Adaptador emparejado",
description = "El adaptador debe estar emparejado en los ajustes Bluetooth de Android",
icon = Icons.Default.CheckCircle
)
}
// Diagnostic commands section
item {
Spacer(modifier = Modifier.height(8.dp))
Text(
"Comandos de Diagnóstico",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
"Ejecuta estos comandos en orden para diagnosticar el problema:",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Command 1: ATZ
item {
DiagnosticCommandCard(
step = "1",
command = "ATZ",
description = "Reset del adaptador",
expectedResponse = "ELM327 v1.5 (o similar)",
isLoading = isLoading,
onSend = { viewModel.sendCommand("ATZ") }
)
}
// Command 2: ATI
item {
DiagnosticCommandCard(
step = "2",
command = "ATI",
description = "Identificación del chip",
expectedResponse = "ELM327 v1.5",
isLoading = isLoading,
onSend = { viewModel.sendCommand("ATI") }
)
}
// Command 3: ATDP
item {
DiagnosticCommandCard(
step = "3",
command = "ATDP",
description = "Protocolo detectado",
expectedResponse = "AUTO, ISO 15765-4 (CAN 11/500)",
isLoading = isLoading,
onSend = { viewModel.sendCommand("ATDP") }
)
}
// Command 4: 0100
item {
DiagnosticCommandCard(
step = "4",
command = "0100",
description = "Test de comunicación con vehículo",
expectedResponse = "41 00 XX XX XX XX (datos en hex)",
isLoading = isLoading,
onSend = { viewModel.sendCommand("0100") }
)
}
// Command 5: 010C
item {
DiagnosticCommandCard(
step = "5",
command = "010C",
description = "RPM del motor (debe funcionar si el motor está encendido)",
expectedResponse = "41 0C XX XX (RPM)",
isLoading = isLoading,
onSend = { viewModel.sendCommand("010C") }
)
}
// Protocol tests
item {
Spacer(modifier = Modifier.height(8.dp))
Text(
"Si nada funciona, prueba protocolos específicos:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
// Protocol buttons
item {
ProtocolTestCard(
protocols = listOf(
"ATSP6" to "CAN 11-bit / 500 kbaud (más común)",
"ATSP7" to "CAN 29-bit / 500 kbaud",
"ATSP8" to "CAN 11-bit / 250 kbaud",
"ATSP9" to "CAN 29-bit / 250 kbaud"
),
isLoading = isLoading,
onSend = { protocol ->
// Send protocol command and then test with 0100
viewModel.sendCommand(protocol)
// Note: User should manually test with 0100 after protocol is set
}
)
}
// Last response
if (lastResponse != null) {
item {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
"Última Respuesta:",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold
)
Text(
"Comando: ${lastResponse.command}",
style = MaterialTheme.typography.bodySmall,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
Text(
"Respuesta: ${lastResponse.rawResponse}",
style = MaterialTheme.typography.bodySmall,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
}
}
}
// Help text
item {
Card {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Info, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(
"Consejos adicionales",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
Text("• Si recibes 'NO DATA', tu vehículo puede no soportar ese comando específico")
Text("• 'CAN ERROR' generalmente indica problema de protocolo o vehículo apagado")
Text("• Algunos vehículos requieren que el motor esté en marcha")
Text("• Los adaptadores ELM327 clones baratos suelen dar problemas")
Text("• Intenta desconectar y reconectar el adaptador del puerto OBD2")
}
}
}
}
}
}
@Composable
fun ChecklistCard(
title: String,
description: String,
icon: androidx.compose.ui.graphics.vector.ImageVector
) {
Card {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
title,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold
)
Text(
description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
fun DiagnosticCommandCard(
step: String,
command: String,
description: String,
expectedResponse: String,
isLoading: Boolean,
onSend: () -> Unit
) {
Card {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
"Paso $step: $command",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Button(
onClick = onSend,
enabled = !isLoading
) {
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null)
}
}
Text(
"Respuesta esperada: $expectedResponse",
style = MaterialTheme.typography.bodySmall,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
color = MaterialTheme.colorScheme.secondary
)
}
}
}
@Composable
fun ProtocolTestCard(
protocols: List<Pair<String, String>>,
isLoading: Boolean,
onSend: (String) -> Unit
) {
Card {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
protocols.forEach { (command, description) ->
Button(
onClick = { onSend(command) },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier.fillMaxWidth()
) {
Text(command, fontWeight = FontWeight.Bold)
Text(
description,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
}

Ver fichero

@@ -27,7 +27,8 @@ fun HomeScreen(
onNavigateToBluetooth: () -> Unit,
onNavigateToVehicleSelection: () -> Unit,
onNavigateToDiagnostic: () -> Unit,
onNavigateToCustomCommand: () -> Unit
onNavigateToCustomCommand: () -> Unit,
onNavigateToDiagnosticHelp: () -> Unit
) {
val connectionState by viewModel.bluetoothService.connectionState.collectAsState()
val selectedDevice by viewModel.selectedDevice.collectAsState()
@@ -126,6 +127,15 @@ fun HomeScreen(
enabled = isConnected
)
// Diagnostic help button
MenuCard(
title = "🔧 Ayuda de Diagnóstico",
description = "¿Problemas con CAN ERROR? Resuelve problemas de conexión",
icon = Icons.Default.Info,
onClick = onNavigateToDiagnosticHelp,
enabled = true
)
// Safety notice
Card(
modifier = Modifier.fillMaxWidth(),