Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ https://github.com/uholeschak/ediabaslib) como motor EDIABAS. No incluye ningún
el usuario copia sus `.prg`/`.grp` a una carpeta del móvil.

## Rutas
- Proyecto: `/home/ridao/Desktop/Pruevas antibraviti/InpaDroid/`
- Proyecto: la raíz de este repositorio
- App: `InpaDroid/src/InpaDroid/InpaDroid.csproj` (namespace raíz `InpaDroid`)
- ediabaslib: `InpaDroid/external/ediabaslib/` (ya copiado, sin `.git`)
- Datos BMW de referencia (NO van dentro de la APK, están en `.gitignore`):
Expand Down
28 changes: 28 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@ InpaDroid is distributed under the GNU GPL v3 (see [LICENSE](LICENSE)). It inclu

EdiabasLib, from the ediabaslib project, vendored under `external/ediabaslib/` (project references to `EdiabasLib` and `UsbSerialNetBinding`). Licensed under the GNU GPL v3. Its license and notices are kept in that directory.

## usb-serial-for-android

USB serial driver for FTDI (K+DCAN) cables, from [mik3y/usb-serial-for-android](https://github.com/mik3y/usb-serial-for-android). It ships in the APK as the prebuilt `usbSerialForAndroid-release.aar` wrapped by `external/ediabaslib/EdiabasLib/AndroidNetBindings/UsbSerialNetBinding`. Licensed under the MIT License (the copyright line must match the upstream LICENSE.txt of the version bundled in the .aar):

```text
MIT License

Copyright (c) the usb-serial-for-android authors (see LICENSE.txt in the upstream repository)

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.
```

## NuGet packages

Versions as declared in `src/InpaDroid/InpaDroid.csproj`. Check each package page on nuget.org for its license text.
Expand Down
5 changes: 3 additions & 2 deletions src/InpaDroid/Diag/AdapterSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ public enum AdapterType { UsbKDcan, Enet, Elm327Bluetooth, Elm327Wifi }
public sealed class AdapterSettings
{
private const string PrefsName = "inpadroid";
public const string DefaultElmWifiHost = "192.168.0.10:35000";

public AdapterType Type { get; set; } = AdapterType.UsbKDcan;
public string BluetoothAddress { get; set; } = ""; // MAC del ELM327
public string EnetHost { get; set; } = EnetHostRule.Default; // "auto", IP o nombre de host
public string ElmWifiHost { get; set; } = "192.168.0.10:35000";
public string ElmWifiHost { get; set; } = DefaultElmWifiHost;
public string EcuPath { get; set; } = "";

public static AdapterSettings Load(Context ctx)
Expand Down Expand Up @@ -54,7 +55,7 @@ public void Save(Context ctx)
}

// <almacenamiento externo de la app>/Ecu. Se crea para que el usuario sepa dónde copiar sus .prg/.grp.
private static string DefaultEcuPath(Context ctx)
public static string DefaultEcuPath(Context ctx)
{
string baseDir = ctx.GetExternalFilesDir(null)?.AbsolutePath ?? ctx.FilesDir?.AbsolutePath ?? "";
string ecuPath = Path.Combine(baseDir, "Ecu");
Expand Down
4 changes: 4 additions & 0 deletions src/InpaDroid/Diag/DiagLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ internal static class DiagLogic
public const string CancelledText = "Cancelled";
public const string ServiceClosedText = "Diagnostic service closed";
public static string TimeoutText(int seconds) => $"Timeout: no response after {seconds} s";
// Prefijo que DiagService antepone al error de EDIABAS si el cable USB está conectado pero sin permiso.
public const string UsbPermissionText = "USB permission not granted";

// Solo códigos EDIABAS cuyo significado consta en la tabla de EdiabasNet (ErrorDescription).
private static readonly Dictionary<string, ErrorKind> CodeKinds = new(StringComparer.Ordinal)
Expand Down Expand Up @@ -74,6 +76,8 @@ public static ErrorKind Classify(string? raw)
return ErrorKind.ServiceClosed;
if (raw.StartsWith("Timeout:", StringComparison.Ordinal))
return ErrorKind.Timeout;
if (raw.StartsWith(UsbPermissionText, StringComparison.Ordinal))
return ErrorKind.UsbPermission;
foreach (Match m in CodeRegex.Matches(raw))
{
if (CodeKinds.TryGetValue(m.Groups[1].Value.ToUpperInvariant() + "-" + m.Groups[2].Value, out var kind))
Expand Down
45 changes: 36 additions & 9 deletions src/InpaDroid/Diag/DiagService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,23 @@ public Task<string> ResolveVariantAsync(string sgbd) =>
return Path.GetFileNameWithoutExtension(_ediabas.SgbdFileName ?? "").ToUpperInvariant();
}, _ => "");

public Task<(double? Ubatt, bool? Ignition)> ReadBatteryIgnitionAsync() =>
Enqueue<(double?, bool?)>(() =>
// Error: texto técnico de la primera lectura que lanzó excepción (null si ninguna), para distinguir un
// adaptador que no abre de un coche que no contesta.
public Task<(double? Ubatt, bool? Ignition, string? Error)> ReadBatteryIgnitionAsync() =>
Enqueue<(double?, bool?, string?)>(() =>
{
string? error = null;
double? ubatt = null;
double? mv = FindNumber(TryRunUtilityJob("STATUS_UBATT"), "STAT_UBATT_WERT", "STAT_UBATT");
double? mv = FindNumber(TryRunUtilityJob("STATUS_UBATT", ref error), "STAT_UBATT_WERT", "STAT_UBATT");
if (mv != null)
{
// UTILITY devuelve mV; si el valor ya parece estar en voltios se deja tal cual.
ubatt = mv > 100 ? mv / 1000.0 : mv;
}

double? ignition = FindNumber(TryRunUtilityJob("STATUS_ZUENDUNG"), "STAT_ZUENDUNG");
return (ubatt, ignition == null ? (bool?)null : ignition != 0);
}, _ => (null, null));
double? ignition = FindNumber(TryRunUtilityJob("STATUS_ZUENDUNG", ref error), "STAT_ZUENDUNG");
return (ubatt, ignition == null ? (bool?)null : ignition != 0, error);
}, error => (null, null, error));

// Cancela el job en curso y los encolados hasta ahora; los que se encolen después no se ven afectados.
// No toca _abort: el hilo de trabajo lo pone a false al empezar cada job y, si coincidiera con este Abort,
Expand Down Expand Up @@ -272,7 +275,30 @@ private static string TimeoutMessage(TimeSpan limit) =>
DiagLogic.TimeoutText((int)limit.TotalSeconds);

// Texto técnico de EDIABAS; la UI lo traduce con DiagLogic.Classify.
private static string DescribeError(Exception ex) => EdiabasNet.GetExceptionText(ex, false, false);
// Sin permiso USB, EDIABAS solo dice que no pudo inicializar la interfaz (IFH-0018); se antepone la causa real.
private string DescribeError(Exception ex)
{
string text = EdiabasNet.GetExceptionText(ex, false, false);
return DiagLogic.Classify(text) == ErrorKind.InterfaceNotFound && UsbPermissionMissing()
? DiagLogic.UsbPermissionText + ": " + text
: text;
}

private bool UsbPermissionMissing()
{
if (_settings.Type != AdapterType.UsbKDcan || _context.GetSystemService(Context.UsbService) is not UsbManager usb)
{
return false;
}
try
{
return EdFtdiInterface.GetDriverList(usb).Any(d => !usb.HasPermission(d.Device));
}
catch (Exception)
{
return false;
}
}

// Cancelado por el watchdog o por un Abort posterior a encolar el job en curso. Se lee en el hilo de trabajo.
private bool IsAborted => _abort || _jobGeneration != Volatile.Read(ref _abortGeneration);
Expand Down Expand Up @@ -438,7 +464,7 @@ private bool IsGroupFile(string sgbd)
return ScanEcuFolder().Any(f => f.IsGroup && f.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}

private List<Dictionary<string, EdiabasNet.ResultData>>? TryRunUtilityJob(string job)
private List<Dictionary<string, EdiabasNet.ResultData>>? TryRunUtilityJob(string job, ref string? error)
{
try
{
Expand All @@ -456,8 +482,9 @@ private bool IsGroupFile(string sgbd)
}
return sets;
}
catch (Exception)
catch (Exception ex)
{
error ??= IsAborted ? DiagLogic.CancelledText : DescribeError(ex);
return null;
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/InpaDroid/Diag/EcuFolderPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Text.RegularExpressions;

namespace InpaDroid.Diag;

/// <summary>Pasa el id de documento del selector de carpetas a la ruta de archivo que necesita EDIABAS.</summary>
public static class EcuFolderPath
{
public const string DefaultPrimaryRoot = "/storage/emulated/0";

/// <summary>
/// "primary:Carpeta" -> {primaryRoot}/Carpeta ; "XXXX-XXXX:Carpeta" -> /storage/XXXX-XXXX/Carpeta.
/// Null si el volumen no es "primary" ni una tarjeta (Descargas, Documentos, etc. no tienen ruta).
/// </summary>
public static string? FromTreeDocumentId(string? docId, string primaryRoot = DefaultPrimaryRoot)
{
docId ??= "";
int colon = docId.IndexOf(':');
string volume = colon >= 0 ? docId[..colon] : docId;
string rel = colon >= 0 ? docId[(colon + 1)..] : "";
bool primary = volume.Equals("primary", StringComparison.OrdinalIgnoreCase);
if (!primary && !Regex.IsMatch(volume, "^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$"))
return null;
string root = primary ? primaryRoot : "/storage/" + volume;
return rel.Length > 0 ? Path.Combine(root, rel) : root;
}
}
1 change: 0 additions & 1 deletion src/InpaDroid/Resources/values-de/strings_ch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<string name="ch_status_heading">Status: %1$s</string>
<string name="ch_reading">Lesen…</string>
<string name="ch_stop">Stopp</string>
<string name="ch_stopped">(gestoppt)</string>
<string name="ch_no_result">FEHLER kein Ergebnis %1$s</string>
<string name="ch_menu_empty">Katalog %1$s ist leer</string>
<string name="ch_menu_pick">Steuergerät wählen (%1$s)</string>
Expand Down
1 change: 0 additions & 1 deletion src/InpaDroid/Resources/values-en/strings_ch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<string name="ch_status_heading">Status: %1$s</string>
<string name="ch_reading">Reading…</string>
<string name="ch_stop">Stop</string>
<string name="ch_stopped">(stopped)</string>
<string name="ch_no_result">ERROR no result %1$s</string>
<string name="ch_menu_empty">%1$s catalog is empty</string>
<string name="ch_menu_pick">Pick an ECU (%1$s)</string>
Expand Down
1 change: 0 additions & 1 deletion src/InpaDroid/Resources/values-pt/strings_ch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<string name="ch_status_heading">Status: %1$s</string>
<string name="ch_reading">Lendo…</string>
<string name="ch_stop">Parar</string>
<string name="ch_stopped">(parado)</string>
<string name="ch_no_result">ERRO sem resultado %1$s</string>
<string name="ch_menu_empty">O catálogo %1$s está vazio</string>
<string name="ch_menu_pick">Escolha uma ECU (%1$s)</string>
Expand Down
1 change: 0 additions & 1 deletion src/InpaDroid/Resources/values/strings_ch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
<string name="ch_status_heading">Status: %1$s</string>
<string name="ch_reading">Leyendo…</string>
<string name="ch_stop">Parar</string>
<string name="ch_stopped">(detenido)</string>
<string name="ch_no_result">ERROR sin resultado %1$s</string>
<string name="ch_menu_empty">Catálogo %1$s vacío</string>
<string name="ch_menu_pick">Elige una centralita (%1$s)</string>
Expand Down
28 changes: 0 additions & 28 deletions src/InpaDroid/Ui/Chassis/ChassisEcuActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ public class ChassisEcuActivity : FaultScreenActivity
ChassisEcu _ecu = null!;
TextView _status = null!;
bool _pollInFlight;
readonly PollRunner _poll = new();

protected override bool PauseHeader => _busyJob || _poll.Running;
protected override bool KeepBusyBar => _poll.Running; // si la operación ha arrancado el Status, su barra "Parar" se queda
protected override bool InFlight => _busyJob || _pollInFlight;

protected override string EcuSgbd => _ecu.Sgbd;
Expand Down Expand Up @@ -88,23 +85,6 @@ protected override void OnCreate(Bundle? savedInstanceState)

void UpdateSubtitle() => SetSubtitle($"{_ecu.Sgbd} → {_sgbd ?? "?"}");

// ---------------------------------------------------------------- control de teclas

protected override bool BeforeKey(int f, bool shift)
{
if (!base.BeforeKey(f, shift))
return false;
// Cualquier acción detiene la lectura continua de Status.
StopPolling();
return true;
}

protected override void OnPause()
{
StopPolling();
base.OnPause();
}

string? StatusError(JobResult r) => DiagLogic.StatusError(r, GetString(Resource.String.g_rp_job_failed)!);

// Botón Reintentar del estado vacío: limpia el aviso y vuelve a identificar; si falla, EnsureSgbdAsync lo muestra de nuevo.
Expand Down Expand Up @@ -337,12 +317,4 @@ void StartPolling(string sgbd, ChassisPage page)
return true;
}, ex => _status.Text = UiUtil.Describe(ex), StatusPollMs, 100);
}

void StopPolling()
{
if (!_poll.Stop())
return;
HideBusy();
_heading.Text += " " + GetString(Resource.String.ch_stopped);
}
}
7 changes: 4 additions & 3 deletions src/InpaDroid/Ui/Chassis/ChassisScanActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,14 @@ async Task<ScanEntry> ScanEcuAsync(ChassisEcu ecu)
{
try
{
var variant = await Diag.ResolveVariantAsync(ecu.Sgbd);
var svc = Diag;
var variant = await svc.ResolveVariantAsync(ecu.Sgbd);
if (string.IsNullOrWhiteSpace(variant))
{
var files = Diag.ScanEcuFolder().Select(f => f.Name);
var files = (await Task.Run(svc.ScanEcuFolder)).Select(f => f.Name);
return new ScanEntry(ecu.Title, ecu.Sgbd, "", ScanSummary.NoVariantState(ecu.Sgbd, files));
}
var r = await Diag.RunJobAsync(variant, ecu.FsReadJob);
var r = await svc.RunJobAsync(variant, ecu.FsReadJob);
var (state, faults, detail) = ScanSummary.Classify(r, GetString(Resource.String.g_rp_job_failed)!);
return new ScanEntry(ecu.Title, ecu.Sgbd, variant, state, faults, detail);
}
Expand Down
8 changes: 4 additions & 4 deletions src/InpaDroid/Ui/CodingActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ void Compare()
Section(sb, Resource.String.cd_cmp_changed, d.Changed.Select(c => $"{c.Key} : {c.Before} → {c.After}").ToList());
Section(sb, Resource.String.cd_cmp_added, d.Added.Select(Line).ToList());
Section(sb, Resource.String.cd_cmp_removed, d.Removed.Select(Line).ToList());
_heading.Text = $"{Title(cur)} · {cur.Job}";
_heading.Text = $"{Label(cur)} · {cur.Job}";
_results.ShowInfo(UiUtil.Str(this, Resource.String.cd_compare_head, FormatWhen(prev)), sb.ToString().TrimEnd());
});
}
Expand All @@ -174,7 +174,7 @@ void Snapshots() =>
return;
}
var adapter = new TwoLineAdapter(this, all.Select(s => new TwoLineAdapter.Row(
Title(s), UiUtil.Str(this, Resource.String.cd_snapshot_sub, FormatWhen(s), JobLabel(s), s.Entries.Count), null, default, s)));
Label(s), UiUtil.Str(this, Resource.String.cd_snapshot_sub, FormatWhen(s), JobLabel(s), s.Entries.Count), null, default, s)));
new AlertDialog.Builder(this)
.SetTitle(GetString(Resource.String.cd_key_snapshots))!
.SetAdapter(adapter, (_, e) => ShowSnapshot((CodingBackup.Snapshot)adapter.GetRow(e.Which).Tag))!
Expand All @@ -185,7 +185,7 @@ void Snapshots() =>
void ShowSnapshot(CodingBackup.Snapshot s)
{
_current = s;
_heading.Text = $"{Title(s)} · {s.Job}";
_heading.Text = $"{Label(s)} · {s.Job}";
_results.ShowInfo($"{s.Sgbd} : {s.Job} {FormatWhen(s)}",
s.Entries.Count == 0 ? GetString(Resource.String.g_rp_no_results)! : string.Join("\n", s.Entries.Select(Line)));
}
Expand All @@ -194,7 +194,7 @@ void ShowSnapshot(CodingBackup.Snapshot s)

static string JobLabel(CodingBackup.Snapshot s) => s.Args.Length > 0 ? $"{s.Job} {s.Args}" : s.Job;

static string Title(CodingBackup.Snapshot s) => s.EcuTitle.Length > 0 ? s.EcuTitle : s.Sgbd;
static string Label(CodingBackup.Snapshot s) => s.EcuTitle.Length > 0 ? s.EcuTitle : s.Sgbd;

static string FormatWhen(CodingBackup.Snapshot s) =>
DateTime.TryParse(s.When, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var t)
Expand Down
22 changes: 19 additions & 3 deletions src/InpaDroid/Ui/ConnectionCheckActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,23 @@ async void RunTest()
bool utility = UpdateFiles();
double? ubatt = null;
bool? ignition = null;
string? error = null;
bool aborted = false;
if (adapterReady && utility)
{
_testing = true;
_testBtn.Enabled = false;
_importBtn.Enabled = false;
_testResult.Text = "";
ShowBusy(GetString(Resource.String.ws_test_busy)!, GetString(Resource.String.g_abort), AbortJob);
ShowBusy(GetString(Resource.String.ws_test_busy)!, GetString(Resource.String.g_abort), () =>
{
aborted = true;
AbortJob();
});
try
{
// Misma lectura de solo lectura que la cabecera, por la cola de DiagService.
(ubatt, ignition) = await Diag.ReadBatteryIgnitionAsync();
(ubatt, ignition, error) = await Diag.ReadBatteryIgnitionAsync();
}
catch (Exception)
{
Expand All @@ -232,6 +238,16 @@ async void RunTest()
HideBusy();
}
}
// Un job abortado no dice nada del coche: diagnosticarlo daría "no contesta".
if (aborted)
{
_testResult.SetTextColor(UiUtil.WarnText);
_testResult.Text = GetString(Resource.String.g_cancelled);
return;
}
// Si el adaptador ni se abrió (sin permiso USB, puerto ocupado), el fallo es suyo y no del coche.
if (ubatt == null && DiagLogic.Classify(error) is ErrorKind.InterfaceNotFound or ErrorKind.UsbPermission or ErrorKind.PortOpen)
adapterReady = false;
string volts = ubatt?.ToString("0.0", CultureInfo.InvariantCulture) ?? "";
bool enet = DiagHolder.LoadSettings(this).Type == AdapterType.Enet;
// Solo importa saber si el móvil está en una red local (WiFi/Ethernet); no se pudo comprobar -> null.
Expand All @@ -240,7 +256,7 @@ async void RunTest()
_testResult.SetTextColor(fault == ConnectionFault.None ? UiUtil.OkText : UiUtil.ErrorText);
_testResult.Text = fault switch
{
ConnectionFault.Adapter => GetString(Resource.String.ws_test_adapter),
ConnectionFault.Adapter => GetString(Resource.String.ws_test_adapter) + (error == null ? "" : "\n" + UiUtil.Friendly(this, error)),
ConnectionFault.EcuFiles => GetString(Resource.String.ws_test_files),
ConnectionFault.NoReply => GetString(Resource.String.ws_test_noreply),
ConnectionFault.EnetNoNetwork => GetString(Resource.String.en_test_nonet),
Expand Down
Loading
Loading