diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt index 45178fa..f6e50c6 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt @@ -584,6 +584,21 @@ object EVHardware { return volts * amps / 1_000f } + /** Raw pack voltage/current used by the OEM charging screen; read-only, SWI68/69. */ + fun getVendorBatteryVoltageV(): Float? { + val gen = FirmwareInfo.getGeneration() + if (gen != FirmwareInfo.Gen.SWI68 && gen != FirmwareInfo.Gen.SWI69) return null + return getFloatPropertyCPM(PROP_BMS_PACK_VOLT_SWI68, AREA_GLOBAL) + ?.takeIf { it.isFinite() && it in PACK_VOLTAGE_RANGE } + } + + fun getVendorBatteryCurrentA(): Float? { + val gen = FirmwareInfo.getGeneration() + if (gen != FirmwareInfo.Gen.SWI68 && gen != FirmwareInfo.Gen.SWI69) return null + return getFloatPropertyCPM(PROP_BMS_PACK_CURRENT_SWI68, AREA_GLOBAL) + ?.takeIf { it.isFinite() && kotlin.math.abs(it) <= MAX_PACK_CURRENT_A } + } + /** Vendor range fallback; SWI68 and SWI69 both publish it on the same id. */ fun getVendorRangeKm(): Int? = if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 || diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt index 54eae63..b28d50e 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergySnapshot.kt @@ -31,6 +31,8 @@ data class EnergySnapshot( val parked: Boolean?, val climate: ClimateSnapshot, val tirePressures: TirePressureSnapshot, + val batteryVoltageV: Float? = null, + val batteryCurrentA: Float? = null, ) { val hasVehicleData: Boolean get() = socPercent != null || rangeKm != null || speedKmh != null || diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt index 92c9b12..1886aec 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/telemetry/EnergyTelemetryReader.kt @@ -26,6 +26,8 @@ interface EnergySignalSource { fun vehicleConsumedKwh(): Float? = null fun vehicleRegeneratedKwh(): Float? = null fun parked(): Boolean? + fun batteryVoltageV(): Float? = null + fun batteryCurrentA(): Float? = null fun climate(): ClimateSnapshot fun tirePressures(): TirePressureSnapshot /** Probe-only values excluded from [EnergySnapshot] and normal consumers. */ @@ -66,6 +68,8 @@ class EvHardwareEnergySignalSource(context: Context) : EnergySignalSource { override fun vehicleConsumedKwh() = SaicCharging.consumedKwhSinceCharge() override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge() override fun parked() = EVHardware.isVehicleInPark() + override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV() + override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA() override fun tirePressures() = TirePressureSnapshot( frontLeftKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_LEFT), frontRightKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_RIGHT), @@ -122,6 +126,8 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) { parked = source.parked(), climate = source.climate(), tirePressures = source.tirePressures(), + batteryVoltageV = source.batteryVoltageV()?.takeIf { it.isFinite() }, + batteryCurrentA = source.batteryCurrentA()?.takeIf { it.isFinite() }, ) /** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */ diff --git a/FatihsMG4-unstable-2.2.19.apk.sha256 b/FatihsMG4-unstable-2.2.19.apk.sha256 new file mode 100644 index 0000000..5ca24f6 --- /dev/null +++ b/FatihsMG4-unstable-2.2.19.apk.sha256 @@ -0,0 +1 @@ +c5b77cde5ece6e7aaffd07dcf74f2259ad4e206eb5bcbb8844549c3470755e78 FatihsMG4-unstable-2.2.19.apk diff --git a/app/build.gradle b/app/build.gradle index 09e78f7..6448170 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.evsuite.abrp" minSdk 27 targetSdk 36 - versionCode 35 - versionName "2.2.18" + versionCode 36 + versionName "2.2.19" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java b/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java index 27364ea..7904157 100644 --- a/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java +++ b/app/src/main/java/com/evsuite/abrp/AbrpUploadService.java @@ -129,6 +129,7 @@ public class AbrpUploadService extends Service { private volatile Location lastLocation; private EnergyTelemetryReader energyReader; + private ConsumptionTracker consumptionTracker; /** * Keeps the unit on the phone hotspot without a trip into Settings. Attempted on the * upload tick because that is exactly when being offline costs something, and it is @@ -194,13 +195,12 @@ public class AbrpUploadService extends Service { // One shared, firmware-aware EVHardware snapshot owns every vehicle signal. energyReader = new EnergyTelemetryReader(getApplicationContext()); + consumptionTracker = ConsumptionTracker.get(this); wifi = new WifiAutoConnect(getApplicationContext()); // The head unit's own weather service, bound for one reason: ENV_OUTSIDE_TEMPERATURE // is not implemented on this vehicle, so the car cannot say how warm it is outside // and something else has to. Idempotent, asynchronous, and absent on a car that does // not have the map stack — in which case ext_temp simply stays unreported. - SaicWeather.INSTANCE.connect(getApplicationContext()); - registerWeatherReceiver(); requestLocationUpdates(); @@ -267,6 +267,7 @@ public class AbrpUploadService extends Service { public void onDestroy() { Log.i(TAG, "Service stopping"); if (scheduler != null) scheduler.shutdownNow(); + if (consumptionTracker != null) consumptionTracker.persist(); if (locationManager != null) { try { locationManager.removeUpdates(locationListener); } catch (Exception ignored) {} } @@ -318,6 +319,10 @@ public class AbrpUploadService extends Service { } private void doUpload() { + long sampleMs = System.currentTimeMillis(); + EnergySnapshot vehicle = energyReader.read(sampleMs); + consumptionTracker.sample(vehicle); + String token = securePrefs.getString(SecurePrefs.KEY_TOKEN, "").trim(); String apiKey = securePrefs.getString(SecurePrefs.KEY_API_KEY, "").trim(); @@ -332,15 +337,12 @@ public class AbrpUploadService extends Service { // Read whatever the car will give us. Every getter returns null when the read // fails, and a null field is omitted from the payload — never sent as 0. - long sampleMs = System.currentTimeMillis(); - EnergySnapshot vehicle = energyReader.read(sampleMs); boolean carUp = vehicle.getHasVehicleData(); Float speedKmh = vehicle.getSpeedKmh(); Integer soc = vehicle.getSocPercent() == null ? null : Math.round(vehicle.getSocPercent()); Integer rangeKm = vehicle.getRangeKm() == null ? null : Math.round(vehicle.getRangeKm()); - Float extTemp = vehicle.getOutsideTempCelsius(); Float powerKw = vehicle.getBatteryPowerKw(); Float chargeRateKw = powerKw == null ? null : -powerKw; Boolean portConnected = vehicle.getChargePortConnected(); @@ -393,40 +395,10 @@ public class AbrpUploadService extends Service { // ENV_OUTSIDE_TEMPERATURE, so there is no vehicle reading to correct. The head unit's // weather service answers for a position, and ambient air at the car is what ext_temp // means — a better figure than a bumper sensor in the sun would give anyway. - String tempSource = "car"; - SaicWeather.Reading sky = null; - if (!TelemetryPayload.isPlausibleTemp(extTemp)) { - // The broadcast first. It is what fills the head unit's own status bar, so on this - // car it is the source known to be producing a number, where the map service's - // query never even bound. - // The broadcast arrives when the weather app feels like it, so a reading that - // never came is retried here rather than only at start-up. - if (headUnitWeather.temperatureC() == null) probeStickyWeather(); - Float broadcast = weatherIsFresh() ? headUnitWeather.temperatureC() : null; - if (TelemetryPayload.isPlausibleTemp(broadcast)) { - extTemp = broadcast; - tempSource = "bcast:" + headUnitWeather.diagnostic(); - } else { - sky = weatherAt(loc); - if (sky != null && sky.getTemperatureCelsius() != null) { - extTemp = sky.getTemperatureCelsius().floatValue(); - tempSource = "map:" + sky.getCity(); - } else { - // Neither answered. Which one failed, and how, is the difference between - // "the broadcast never arrives" and "it arrives in a shape this cannot - // read" — and nothing on the ABRP side tells them apart. - tempSource = "none bcast=" + headUnitWeather.diagnostic() - + " map=" + (SaicWeather.INSTANCE.isAvailable() ? "bound" : "unbound") - + (lastQueryMs < 0 ? "" : "/" + lastQueryMs + "ms"); - } - } - } - TelemetryPayload tlm = new TelemetryPayload(System.currentTimeMillis() / 1000); tlm.soc = soc; tlm.speedKmh = speedKmh; tlm.rangeKm = rangeKm; - tlm.extTemp = extTemp; tlm.powerKw = powerKw; tlm.charging = charging; tlm.dcfc = dcfc; @@ -450,7 +422,7 @@ public class AbrpUploadService extends Service { } String tlmJson = tlm.build(); - String summary = TelemetryPayload.summarize(tlmJson) + " [temp " + tempSource + "]"; + String summary = TelemetryPayload.summarize(tlmJson); if (loc != null) { summary += " (fix " + locationAgeMs(loc) / 1000 + "s old" + " +/-" + (loc.hasAccuracy() ? Math.round(loc.getAccuracy()) + "m" : "?") @@ -458,7 +430,6 @@ public class AbrpUploadService extends Service { // The weather service names the place it answered for. ABRP shows an address it // reverse-geocodes from the same coordinates, so the two disagreeing says the // coordinates are wrong, and the two agreeing says ABRP is showing something old. - if (sky != null && !sky.getCity().isEmpty()) summary += ", near " + sky.getCity(); summary += ")"; } // What actually went on the wire. Kept on the log entry as well as in logcat: the diff --git a/app/src/main/java/com/evsuite/abrp/ChargingGraphView.java b/app/src/main/java/com/evsuite/abrp/ChargingGraphView.java new file mode 100644 index 0000000..eb71751 --- /dev/null +++ b/app/src/main/java/com/evsuite/abrp/ChargingGraphView.java @@ -0,0 +1,29 @@ +package com.evsuite.abrp; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import java.util.ArrayList; +import java.util.List; + +/** Lightweight SOC/power charging curve, avoiding a heavy chart dependency on AAOS 9. */ +public final class ChargingGraphView extends View { + private final Paint grid = new Paint(1), line = new Paint(1), text = new Paint(1); + private final List points = new ArrayList<>(); + public ChargingGraphView(Context c, AttributeSet a) { super(c,a); grid.setColor(0x3348C7D9); line.setColor(0xFFFFC857); line.setStrokeWidth(4); text.setColor(0xFFBDC4C6); text.setTextSize(24); } + synchronized void add(Float soc, Float power) { + if (soc == null || power == null || power >= 0) return; + float p = Math.abs(power); + if (!points.isEmpty() && Math.abs(points.get(points.size()-1)[0] - soc) < .1f) points.remove(points.size()-1); + points.add(new float[]{soc,p}); if (points.size() > 500) points.remove(0); invalidate(); + } + @Override protected synchronized void onDraw(Canvas c) { + super.onDraw(c); float w=getWidth(), h=getHeight()-34; + for(int i=0;i<=4;i++) c.drawLine(0,h*i/4,w,h*i/4,grid); + c.drawText("SOC % → Şarj gücü (kW)",12,getHeight()-5,text); + float max=20; for(float[] p:points) max=Math.max(max,p[1]); + for(int i=1;i 0 && now > lastMs) { + double hours = (now - lastMs) / 3_600_000d; + // Never bridge a process pause/sleep into invented driving data. + if (hours <= 0.1d) { + Float speed = s.getSpeedKmh(); + Float power = s.getBatteryPowerKw(); + double km = speed == null ? 0d : Math.max(0d, + ((lastSpeed == null ? speed : lastSpeed) + speed) * 0.5d) * hours; + double kwh = power == null ? 0d : + ((lastPower == null ? power : lastPower) + power) * 0.5d * hours; + double dsoc = (lastSoc == null || s.getSocPercent() == null || + Boolean.TRUE.equals(s.getChargePortConnected())) ? 0d : lastSoc - s.getSocPercent(); + start = add(start, km, kwh, hours, dsoc); + lifetime = add(lifetime, km, kwh, hours, dsoc); + tripA = add(tripA, km, kwh, hours, dsoc); + tripB = add(tripB, km, kwh, hours, dsoc); + if (Boolean.FALSE.equals(parked)) motor = add(motor, km, kwh, hours, dsoc); + persist(); + } + } + lastMs = now; lastSpeed = s.getSpeedKmh(); lastPower = s.getBatteryPowerKw(); + lastSoc = s.getSocPercent(); lastParked = parked; + } + + synchronized Totals totals(Period period) { + switch (period) { + case START: return start; case MOTOR: return motor; case TRIP_A: return tripA; + case TRIP_B: return tripB; default: return lifetime; + } + } + + synchronized void reset(Period period) { + if (period == Period.TRIP_A) tripA = zero(); + else if (period == Period.TRIP_B) tripB = zero(); + else return; + persist(); + } + + synchronized void persist() { + SharedPreferences.Editor e = prefs.edit(); + save(e, "life", lifetime); save(e, "a", tripA); save(e, "b", tripB); + e.commit(); + } + + private Totals load(String key) { return new Totals(bits(key,"km"), bits(key,"kwh"), bits(key,"h"), bits(key,"soc")); } + private double bits(String key, String field) { return Double.longBitsToDouble(prefs.getLong(key + "_" + field, 0L)); } + private static void save(SharedPreferences.Editor e, String key, Totals t) { + e.putLong(key+"_km", Double.doubleToRawLongBits(t.km)); + e.putLong(key+"_kwh", Double.doubleToRawLongBits(t.kwh)); + e.putLong(key+"_h", Double.doubleToRawLongBits(t.hours)); + e.putLong(key+"_soc", Double.doubleToRawLongBits(t.soc)); + } + private static Totals add(Totals t, double km, double kwh, double h, double soc) { + return new Totals(t.km + km, t.kwh + kwh, t.hours + h, t.soc + soc); + } + private static Totals zero() { return new Totals(0,0,0,0); } +} diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java index 1334a65..b4d5d54 100644 --- a/app/src/main/java/com/evsuite/abrp/MainActivity.java +++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java @@ -46,15 +46,15 @@ public class MainActivity extends AppCompatActivity { /** Navigation destinations in page order. Parallel to {@link #panes}. */ private static final int[] TAB_IDS = - { R.id.tabVehicle, R.id.tabAbrp, R.id.tabService, R.id.tabWifi, + { R.id.tabVehicle, R.id.tabConsumption, R.id.tabCharging, R.id.tabAbrp, R.id.tabService, R.id.tabWifi, R.id.tabLog }; private static final int[] PAGE_TITLES = { - R.string.design_overview_title, R.string.design_abrp_title, + R.string.design_overview_title, R.string.consumption_title, R.string.charging_title, R.string.design_abrp_title, R.string.design_service_title, R.string.design_wifi_title, R.string.design_log_title }; private static final int[] PAGE_SUBTITLES = { - R.string.design_overview_subtitle, R.string.design_abrp_subtitle, + R.string.design_overview_subtitle, R.string.consumption_subtitle, R.string.charging_subtitle, R.string.design_abrp_subtitle, R.string.design_service_subtitle, R.string.design_wifi_subtitle, R.string.design_log_subtitle }; @@ -85,6 +85,8 @@ public class MainActivity extends AppCompatActivity { private View servicePane; private View logPane; private View vehiclePane; + private View consumptionPane; + private View chargingPane; private View wifiPane; /** The five pages in navigation order. Parallel to {@link #TAB_IDS}. */ @@ -103,8 +105,18 @@ public class MainActivity extends AppCompatActivity { */ private EnergyTelemetryReader vehicleReader; private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer, - vehCharging, vehExtTemp, vehHvac, vehSoe, + vehCharging, vehHvac, vehSoe, vehTireFl, vehTireFr, vehTireRl, vehTireRr; + private ConsumptionTracker consumptionTracker; + private Spinner consumptionPeriod; + private TextView consumptionDistance, consumptionEnergy, consumptionAverage, + consumptionSpeed, consumptionTime, consumptionSoc; + private Button consumptionReset; + private TextView chargeStatus, chargeSoc, chargeVoltage, chargeCurrent, chargePower, + chargeEnergy, chargeDuration; + private ChargingGraphView chargeGraph; + private long chargeStartMs, lastChargeMs; + private double chargedKwh; /** * Inflates a page with no parent, then gives it the MATCH_PARENT/MATCH_PARENT layout @@ -181,6 +193,7 @@ public class MainActivity extends AppCompatActivity { refreshStatus(); refreshCallLog(); refreshVehicle(); + refreshConsumption(); refreshWifi(); uiHandler.postDelayed(this, 2_000L); } @@ -219,11 +232,15 @@ public class MainActivity extends AppCompatActivity { abrpPane = inflatePane(R.layout.pane_abrp); servicePane = inflatePane(R.layout.pane_service); vehiclePane = inflatePane(R.layout.pane_vehicle); + consumptionPane = inflatePane(R.layout.pane_consumption); + chargingPane = inflatePane(R.layout.pane_charging); wifiPane = inflatePane(R.layout.pane_wifi); logPane = inflatePane(R.layout.pane_log); - panes = new View[] { vehiclePane, abrpPane, servicePane, wifiPane, logPane }; + panes = new View[] { vehiclePane, consumptionPane, chargingPane, abrpPane, servicePane, wifiPane, logPane }; bindVehiclePane(); + bindConsumptionPane(); + bindChargingPane(); bindWifiPane(); apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout); @@ -761,7 +778,6 @@ public class MainActivity extends AppCompatActivity { vehSpeed = vehiclePane.findViewById(R.id.vehicle_speed); vehOdometer = vehiclePane.findViewById(R.id.vehicle_odometer); vehCharging = vehiclePane.findViewById(R.id.vehicle_charging); - vehExtTemp = vehiclePane.findViewById(R.id.vehicle_ext_temp); vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac); vehSoe = vehiclePane.findViewById(R.id.vehicle_soe); vehTireFl = vehiclePane.findViewById(R.id.vehicle_tire_fl); @@ -784,13 +800,14 @@ public class MainActivity extends AppCompatActivity { } // The timestamp is passed explicitly: its Kotlin default is not visible from Java. EnergySnapshot s = vehicleReader.read(System.currentTimeMillis()); + ConsumptionTracker.get(this).sample(s); + refreshCharging(s); vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%")); vehRange.setText(fmt(s.getRangeKm(), "%.0f km")); vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW")); vehSpeed.setText(fmt(s.getSpeedKmh(), "%.0f km/h")); vehOdometer.setText(fmt(s.getOdometerKm(), "%.0f km")); - vehExtTemp.setText(fmt(s.getOutsideTempCelsius(), "%.0f °C")); vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C")); vehSoe.setText(fmt(s.getBatteryEnergyKwh(), "%.1f kWh")); @@ -805,6 +822,94 @@ public class MainActivity extends AppCompatActivity { } } + private void bindConsumptionPane() { + consumptionTracker = ConsumptionTracker.get(this); + consumptionPeriod = consumptionPane.findViewById(R.id.consumption_period); + String[] periods = {"Başlangıçtan itibaren", "Motor çalıştıktan itibaren", + "Tüm zamanlar", "Trip A", "Trip B"}; + ArrayAdapter adapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_dropdown_item, periods); + consumptionPeriod.setAdapter(adapter); + consumptionDistance = consumptionPane.findViewById(R.id.consumption_distance); + consumptionEnergy = consumptionPane.findViewById(R.id.consumption_energy); + consumptionAverage = consumptionPane.findViewById(R.id.consumption_average); + consumptionSpeed = consumptionPane.findViewById(R.id.consumption_speed); + consumptionTime = consumptionPane.findViewById(R.id.consumption_time); + consumptionSoc = consumptionPane.findViewById(R.id.consumption_soc); + consumptionReset = consumptionPane.findViewById(R.id.consumption_reset); + consumptionPeriod.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(AdapterView p, View v, int position, long id) { + consumptionReset.setVisibility(position >= 3 ? View.VISIBLE : View.GONE); + refreshConsumption(); + } + @Override public void onNothingSelected(AdapterView p) {} + }); + consumptionReset.setOnClickListener(v -> { + int selected = consumptionPeriod.getSelectedItemPosition(); + if (selected < 3) return; + String name = selected == 3 ? "Trip A" : "Trip B"; + new MaterialAlertDialogBuilder(this).setTitle(name + " sıfırlansın mı?") + .setMessage("Bu sayacın kayıtlı tüketim geçmişi kalıcı olarak silinir.") + .setNegativeButton("Vazgeç", null) + .setPositiveButton("Sıfırla", (d, w) -> { + consumptionTracker.reset(selected == 3 + ? ConsumptionTracker.Period.TRIP_A : ConsumptionTracker.Period.TRIP_B); + refreshConsumption(); + }).show(); + }); + } + + private void bindChargingPane() { + chargeStatus = chargingPane.findViewById(R.id.charge_status); + chargeSoc = chargingPane.findViewById(R.id.charge_soc); + chargeVoltage = chargingPane.findViewById(R.id.charge_voltage); + chargeCurrent = chargingPane.findViewById(R.id.charge_current); + chargePower = chargingPane.findViewById(R.id.charge_power); + chargeEnergy = chargingPane.findViewById(R.id.charge_energy); + chargeDuration = chargingPane.findViewById(R.id.charge_duration); + chargeGraph = chargingPane.findViewById(R.id.charge_graph); + } + + private void refreshCharging(EnergySnapshot s) { + if (chargeStatus == null) return; + Integer code = s.getChargingStatus(); + boolean charging = (code != null && (code == 1 || code == 10)) || + (Boolean.TRUE.equals(s.getChargePortConnected()) && s.getBatteryPowerKw() != null + && s.getBatteryPowerKw() < -0.2f); + long now = s.getTimestampMs(); + if (charging && chargeStartMs == 0) { chargeStartMs = now; chargedKwh = 0; } + if (!charging) { chargeStartMs = 0; lastChargeMs = 0; chargedKwh = 0; } + if (charging && lastChargeMs > 0 && s.getBatteryPowerKw() != null) + chargedKwh += Math.max(0, -s.getBatteryPowerKw()) * (now-lastChargeMs) / 3_600_000d; + if (charging) lastChargeMs = now; + chargeStatus.setText(charging ? (code != null && code == 10 ? "DC hızlı şarj" : "AC şarj") : "Şarj yok"); + chargeSoc.setText(fmt(s.getSocPercent(), "%.1f %%")); + chargeVoltage.setText(fmt(s.getBatteryVoltageV(), "%.1f V")); + chargeCurrent.setText(fmt(s.getBatteryCurrentA(), "%.1f A")); + chargePower.setText(s.getBatteryPowerKw() == null ? "—" : String.format(java.util.Locale.getDefault(), "%.1f kW", Math.abs(s.getBatteryPowerKw()))); + chargeEnergy.setText(charging ? String.format(java.util.Locale.getDefault(), "%.2f kWh", chargedKwh) : "—"); + long sec = chargeStartMs == 0 ? 0 : (now-chargeStartMs)/1000; + chargeDuration.setText(charging ? String.format(java.util.Locale.getDefault(), "%02d:%02d:%02d", sec/3600,(sec%3600)/60,sec%60) : "—"); + if (charging) chargeGraph.add(s.getSocPercent(), s.getBatteryPowerKw()); + } + + private void refreshConsumption() { + if (consumptionTracker == null || consumptionPeriod == null) return; + ConsumptionTracker.Period period = ConsumptionTracker.Period.values()[ + Math.max(0, consumptionPeriod.getSelectedItemPosition())]; + ConsumptionTracker.Totals t = consumptionTracker.totals(period); + consumptionDistance.setText(String.format(java.util.Locale.getDefault(), "%.1f km", t.km)); + consumptionEnergy.setText(String.format(java.util.Locale.getDefault(), "%.2f kWh", t.kwh)); + consumptionAverage.setText(t.km < 0.1 ? "—" : String.format(java.util.Locale.getDefault(), + "%.1f kWh/100 km", t.kwh * 100d / t.km)); + consumptionSpeed.setText(t.hours <= 0 ? "—" : String.format(java.util.Locale.getDefault(), + "%.1f km/h", t.km / t.hours)); + long minutes = Math.round(t.hours * 60d); + consumptionTime.setText(String.format(java.util.Locale.getDefault(), "%d sa %02d dk", + minutes / 60, minutes % 60)); + consumptionSoc.setText(String.format(java.util.Locale.getDefault(), "%.1f %%", t.soc)); + } + /** * The charging row: the session state when the car reports one, the port otherwise. * diff --git a/app/src/main/java/com/evsuite/abrp/TelemetryPayload.java b/app/src/main/java/com/evsuite/abrp/TelemetryPayload.java index 3200dea..54174b9 100644 --- a/app/src/main/java/com/evsuite/abrp/TelemetryPayload.java +++ b/app/src/main/java/com/evsuite/abrp/TelemetryPayload.java @@ -35,7 +35,6 @@ final class TelemetryPayload { // --- Lower priority --- Integer rangeKm; // km, vehicle's own estimate - Float extTemp; // °C, outside Float cabinTemp; // °C Float battTempC; // °C, traction battery Float capacityKwh; // kWh, usable pack capacity @@ -63,13 +62,6 @@ final class TelemetryPayload { putIfPresent(tlm, "soc", soc); putIfPresent(tlm, "speed", speedKmh == null ? null : Math.round(speedKmh)); putIfPresent(tlm, "est_battery_range", rangeKm); - // Outside temp gets the same plausibility guard as the other temperatures: - // a VHAL that does not implement ENV_OUTSIDE_TEMPERATURE answers 0.0, and - // ABRP treats "0 °C outside" as a real reading that costs range in its plan. - // A genuine 0 °C is lost with it, which is the cheaper of the two errors. - if (isPlausibleTemp(extTemp)) { - tlm.put("ext_temp", Math.round(extTemp)); - } if (powerKw != null) tlm.put("power", round2(powerKw)); putIfPresent(tlm, "is_charging", boolToInt(charging)); putIfPresent(tlm, "is_dcfc", boolToInt(dcfc)); @@ -169,7 +161,7 @@ final class TelemetryPayload { * Used only to work out what was left out — see {@link #summarize(String)}. */ private static final String[] ALL_FIELDS = { - "utc", "soc", "speed", "power", "est_battery_range", "ext_temp", "cabin_temp", + "utc", "soc", "speed", "power", "est_battery_range", "cabin_temp", "batt_temp", "hvac_setpoint", "capacity", "soe", "kwh_charged", "odometer", "is_charging", "is_dcfc", "is_parked", "lat", "lon", "elevation", "heading", "tire_pressure_fl", "tire_pressure_fr", "tire_pressure_rl", "tire_pressure_rr", diff --git a/app/src/main/res/layout-w1000dp/activity_main.xml b/app/src/main/res/layout-w1000dp/activity_main.xml index 3d9a95c..8a3adb2 100644 --- a/app/src/main/res/layout-w1000dp/activity_main.xml +++ b/app/src/main/res/layout-w1000dp/activity_main.xml @@ -7,6 +7,8 @@ + + diff --git a/app/src/main/res/layout-w1000dp/pane_vehicle.xml b/app/src/main/res/layout-w1000dp/pane_vehicle.xml index 95f564e..6419b28 100644 --- a/app/src/main/res/layout-w1000dp/pane_vehicle.xml +++ b/app/src/main/res/layout-w1000dp/pane_vehicle.xml @@ -9,10 +9,6 @@ - - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4203928..b61ea7a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -14,6 +14,8 @@ + + diff --git a/app/src/main/res/layout/pane_charging.xml b/app/src/main/res/layout/pane_charging.xml new file mode 100644 index 0000000..fbcbe3d --- /dev/null +++ b/app/src/main/res/layout/pane_charging.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pane_consumption.xml b/app/src/main/res/layout/pane_consumption.xml new file mode 100644 index 0000000..0a711cd --- /dev/null +++ b/app/src/main/res/layout/pane_consumption.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pane_vehicle.xml b/app/src/main/res/layout/pane_vehicle.xml index e852c2c..006635e 100644 --- a/app/src/main/res/layout/pane_vehicle.xml +++ b/app/src/main/res/layout/pane_vehicle.xml @@ -8,10 +8,6 @@ - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46835e0..1ca66e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,12 @@ ABRP Aktarım Genel + Tüketim + Şarj + Tüketim + Başlangıç, motor, tüm zamanlar ve trip sayaçları + Şarj + Canlı batarya değerleri ve SOC/güç eğrisi Wi-Fi Kayıtlar @@ -28,7 +34,6 @@ Hız Kilometre Batarya Sıcaklığı - Dış Sıcaklık Kabin Sıcaklığı Klima Ayarı Batarya Kapasitesi diff --git a/app/src/test/java/com/evsuite/abrp/TelemetryPayloadTest.java b/app/src/test/java/com/evsuite/abrp/TelemetryPayloadTest.java index 61af2e4..dc184a0 100644 --- a/app/src/test/java/com/evsuite/abrp/TelemetryPayloadTest.java +++ b/app/src/test/java/com/evsuite/abrp/TelemetryPayloadTest.java @@ -21,7 +21,6 @@ public class TelemetryPayloadTest { t.soc = 63; t.speedKmh = 48.4f; t.rangeKm = 210; - t.extTemp = 12f; t.powerKw = -14.5f; t.charging = false; t.dcfc = false; @@ -46,7 +45,7 @@ public class TelemetryPayloadTest { public void unreadablePropertiesAreOmittedNotZeroed() { String json = noCar(); for (String field : new String[]{ - "soc", "speed", "est_battery_range", "ext_temp", "power", + "soc", "speed", "est_battery_range", "power", "is_charging", "is_dcfc", "is_parked", "cabin_temp", "batt_temp", "capacity", "odometer", "hvac_setpoint", "tire_pressure_fl", "tire_pressure_fr", @@ -84,7 +83,6 @@ public class TelemetryPayloadTest { assertEquals(63, json.getInt("soc")); assertEquals(48, json.getInt("speed")); // rounded assertEquals(210, json.getInt("est_battery_range")); - assertEquals(12, json.getInt("ext_temp")); assertEquals(-14.5, json.getDouble("power"), 0.001); assertEquals(0, json.getInt("is_charging")); assertEquals(0, json.getInt("is_dcfc")); @@ -296,27 +294,10 @@ public class TelemetryPayloadTest { new JSONObject(noCar()); } - @Test - public void unimplementedOutsideTempIsOmittedNotSentAsZero() { - // A VHAL without ENV_OUTSIDE_TEMPERATURE answers 0.0. Sent as ext_temp:0 it is a - // freezing day as far as ABRP's consumption model is concerned. - TelemetryPayload t = fullCar(); - t.extTemp = 0f; - assertFalse(t.build().contains("ext_temp")); - } - - @Test - public void impossibleOutsideTempIsOmitted() { - TelemetryPayload t = fullCar(); - t.extTemp = -273f; - assertFalse(t.build().contains("ext_temp")); - } - @Test public void summaryListsSentValuesAndOmittedFields() { String summary = TelemetryPayload.summarize(fullCar().build()); assertTrue(summary, summary.contains("soc=63")); - assertTrue(summary, summary.contains("ext_temp=12")); assertTrue(summary, summary.contains("cabin_temp=22")); // Nothing gave us a position, so lat/lon must show up as omitted, not as 0. assertFalse(summary, summary.contains("lat="));