38
README.md
38
README.md
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
Referencia en una nueva incidencia
Block a user