Add persistent consumption and charging dashboards
This commit is contained in:
@@ -584,6 +584,21 @@ object EVHardware {
|
|||||||
return volts * amps / 1_000f
|
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. */
|
/** Vendor range fallback; SWI68 and SWI69 both publish it on the same id. */
|
||||||
fun getVendorRangeKm(): Int? =
|
fun getVendorRangeKm(): Int? =
|
||||||
if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 ||
|
if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 ||
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ data class EnergySnapshot(
|
|||||||
val parked: Boolean?,
|
val parked: Boolean?,
|
||||||
val climate: ClimateSnapshot,
|
val climate: ClimateSnapshot,
|
||||||
val tirePressures: TirePressureSnapshot,
|
val tirePressures: TirePressureSnapshot,
|
||||||
|
val batteryVoltageV: Float? = null,
|
||||||
|
val batteryCurrentA: Float? = null,
|
||||||
) {
|
) {
|
||||||
val hasVehicleData: Boolean
|
val hasVehicleData: Boolean
|
||||||
get() = socPercent != null || rangeKm != null || speedKmh != null ||
|
get() = socPercent != null || rangeKm != null || speedKmh != null ||
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ interface EnergySignalSource {
|
|||||||
fun vehicleConsumedKwh(): Float? = null
|
fun vehicleConsumedKwh(): Float? = null
|
||||||
fun vehicleRegeneratedKwh(): Float? = null
|
fun vehicleRegeneratedKwh(): Float? = null
|
||||||
fun parked(): Boolean?
|
fun parked(): Boolean?
|
||||||
|
fun batteryVoltageV(): Float? = null
|
||||||
|
fun batteryCurrentA(): Float? = null
|
||||||
fun climate(): ClimateSnapshot
|
fun climate(): ClimateSnapshot
|
||||||
fun tirePressures(): TirePressureSnapshot
|
fun tirePressures(): TirePressureSnapshot
|
||||||
/** Probe-only values excluded from [EnergySnapshot] and normal consumers. */
|
/** 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 vehicleConsumedKwh() = SaicCharging.consumedKwhSinceCharge()
|
||||||
override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge()
|
override fun vehicleRegeneratedKwh() = SaicCharging.regeneratedKwhSinceCharge()
|
||||||
override fun parked() = EVHardware.isVehicleInPark()
|
override fun parked() = EVHardware.isVehicleInPark()
|
||||||
|
override fun batteryVoltageV() = EVHardware.getVendorBatteryVoltageV()
|
||||||
|
override fun batteryCurrentA() = EVHardware.getVendorBatteryCurrentA()
|
||||||
override fun tirePressures() = TirePressureSnapshot(
|
override fun tirePressures() = TirePressureSnapshot(
|
||||||
frontLeftKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_LEFT),
|
frontLeftKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_LEFT),
|
||||||
frontRightKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_RIGHT),
|
frontRightKpa = EVHardware.getTirePressureKpa(EVHardware.Wheel.FRONT_RIGHT),
|
||||||
@@ -122,6 +126,8 @@ class EnergyTelemetryReader(private val source: EnergySignalSource) {
|
|||||||
parked = source.parked(),
|
parked = source.parked(),
|
||||||
climate = source.climate(),
|
climate = source.climate(),
|
||||||
tirePressures = source.tirePressures(),
|
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. */
|
/** Reads CP-004 candidates only when an unstable evidence capture explicitly requests them. */
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
c5b77cde5ece6e7aaffd07dcf74f2259ad4e206eb5bcbb8844549c3470755e78 FatihsMG4-unstable-2.2.19.apk
|
||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.evsuite.abrp"
|
applicationId "com.evsuite.abrp"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 35
|
versionCode 36
|
||||||
versionName "2.2.18"
|
versionName "2.2.19"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ public class AbrpUploadService extends Service {
|
|||||||
private volatile Location lastLocation;
|
private volatile Location lastLocation;
|
||||||
|
|
||||||
private EnergyTelemetryReader energyReader;
|
private EnergyTelemetryReader energyReader;
|
||||||
|
private ConsumptionTracker consumptionTracker;
|
||||||
/**
|
/**
|
||||||
* Keeps the unit on the phone hotspot without a trip into Settings. Attempted on the
|
* 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
|
* 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.
|
// One shared, firmware-aware EVHardware snapshot owns every vehicle signal.
|
||||||
energyReader = new EnergyTelemetryReader(getApplicationContext());
|
energyReader = new EnergyTelemetryReader(getApplicationContext());
|
||||||
|
consumptionTracker = ConsumptionTracker.get(this);
|
||||||
wifi = new WifiAutoConnect(getApplicationContext());
|
wifi = new WifiAutoConnect(getApplicationContext());
|
||||||
// The head unit's own weather service, bound for one reason: ENV_OUTSIDE_TEMPERATURE
|
// 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
|
// 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
|
// 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.
|
// not have the map stack — in which case ext_temp simply stays unreported.
|
||||||
SaicWeather.INSTANCE.connect(getApplicationContext());
|
|
||||||
registerWeatherReceiver();
|
|
||||||
|
|
||||||
requestLocationUpdates();
|
requestLocationUpdates();
|
||||||
|
|
||||||
@@ -267,6 +267,7 @@ public class AbrpUploadService extends Service {
|
|||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
Log.i(TAG, "Service stopping");
|
Log.i(TAG, "Service stopping");
|
||||||
if (scheduler != null) scheduler.shutdownNow();
|
if (scheduler != null) scheduler.shutdownNow();
|
||||||
|
if (consumptionTracker != null) consumptionTracker.persist();
|
||||||
if (locationManager != null) {
|
if (locationManager != null) {
|
||||||
try { locationManager.removeUpdates(locationListener); } catch (Exception ignored) {}
|
try { locationManager.removeUpdates(locationListener); } catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
@@ -318,6 +319,10 @@ public class AbrpUploadService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doUpload() {
|
private void doUpload() {
|
||||||
|
long sampleMs = System.currentTimeMillis();
|
||||||
|
EnergySnapshot vehicle = energyReader.read(sampleMs);
|
||||||
|
consumptionTracker.sample(vehicle);
|
||||||
|
|
||||||
String token = securePrefs.getString(SecurePrefs.KEY_TOKEN, "").trim();
|
String token = securePrefs.getString(SecurePrefs.KEY_TOKEN, "").trim();
|
||||||
String apiKey = securePrefs.getString(SecurePrefs.KEY_API_KEY, "").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
|
// 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.
|
// 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();
|
boolean carUp = vehicle.getHasVehicleData();
|
||||||
Float speedKmh = vehicle.getSpeedKmh();
|
Float speedKmh = vehicle.getSpeedKmh();
|
||||||
Integer soc = vehicle.getSocPercent() == null
|
Integer soc = vehicle.getSocPercent() == null
|
||||||
? null : Math.round(vehicle.getSocPercent());
|
? null : Math.round(vehicle.getSocPercent());
|
||||||
Integer rangeKm = vehicle.getRangeKm() == null
|
Integer rangeKm = vehicle.getRangeKm() == null
|
||||||
? null : Math.round(vehicle.getRangeKm());
|
? null : Math.round(vehicle.getRangeKm());
|
||||||
Float extTemp = vehicle.getOutsideTempCelsius();
|
|
||||||
Float powerKw = vehicle.getBatteryPowerKw();
|
Float powerKw = vehicle.getBatteryPowerKw();
|
||||||
Float chargeRateKw = powerKw == null ? null : -powerKw;
|
Float chargeRateKw = powerKw == null ? null : -powerKw;
|
||||||
Boolean portConnected = vehicle.getChargePortConnected();
|
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
|
// 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
|
// 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.
|
// 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);
|
TelemetryPayload tlm = new TelemetryPayload(System.currentTimeMillis() / 1000);
|
||||||
tlm.soc = soc;
|
tlm.soc = soc;
|
||||||
tlm.speedKmh = speedKmh;
|
tlm.speedKmh = speedKmh;
|
||||||
tlm.rangeKm = rangeKm;
|
tlm.rangeKm = rangeKm;
|
||||||
tlm.extTemp = extTemp;
|
|
||||||
tlm.powerKw = powerKw;
|
tlm.powerKw = powerKw;
|
||||||
tlm.charging = charging;
|
tlm.charging = charging;
|
||||||
tlm.dcfc = dcfc;
|
tlm.dcfc = dcfc;
|
||||||
@@ -450,7 +422,7 @@ public class AbrpUploadService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String tlmJson = tlm.build();
|
String tlmJson = tlm.build();
|
||||||
String summary = TelemetryPayload.summarize(tlmJson) + " [temp " + tempSource + "]";
|
String summary = TelemetryPayload.summarize(tlmJson);
|
||||||
if (loc != null) {
|
if (loc != null) {
|
||||||
summary += " (fix " + locationAgeMs(loc) / 1000 + "s old"
|
summary += " (fix " + locationAgeMs(loc) / 1000 + "s old"
|
||||||
+ " +/-" + (loc.hasAccuracy() ? Math.round(loc.getAccuracy()) + "m" : "?")
|
+ " +/-" + (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
|
// 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
|
// 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.
|
// coordinates are wrong, and the two agreeing says ABRP is showing something old.
|
||||||
if (sky != null && !sky.getCity().isEmpty()) summary += ", near " + sky.getCity();
|
|
||||||
summary += ")";
|
summary += ")";
|
||||||
}
|
}
|
||||||
// What actually went on the wire. Kept on the log entry as well as in logcat: the
|
// What actually went on the wire. Kept on the log entry as well as in logcat: the
|
||||||
|
|||||||
@@ -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<float[]> 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<points.size();i++){float[] a=points.get(i-1),b=points.get(i); c.drawLine(a[0]*w/100,h-a[1]*h/max,b[0]*w/100,h-b[1]*h/max,line);}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import com.evsuite.hardware.telemetry.EnergySnapshot;
|
||||||
|
|
||||||
|
/** Persistent, read-only trip/lifetime integration patterned after DriveHub_Dort. */
|
||||||
|
final class ConsumptionTracker {
|
||||||
|
enum Period { START, MOTOR, LIFETIME, TRIP_A, TRIP_B }
|
||||||
|
|
||||||
|
static final class Totals {
|
||||||
|
final double km, kwh, hours, soc;
|
||||||
|
Totals(double km, double kwh, double hours, double soc) {
|
||||||
|
this.km = km; this.kwh = kwh; this.hours = hours; this.soc = soc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String PREFS = "consumption_counters_v1";
|
||||||
|
private static ConsumptionTracker instance;
|
||||||
|
static synchronized ConsumptionTracker get(Context context) {
|
||||||
|
if (instance == null) instance = new ConsumptionTracker(context.getApplicationContext());
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
private Totals start = zero(), motor = zero(), lifetime, tripA, tripB;
|
||||||
|
private long lastMs;
|
||||||
|
private Float lastSpeed, lastPower, lastSoc;
|
||||||
|
private Boolean lastParked;
|
||||||
|
|
||||||
|
private ConsumptionTracker(Context context) {
|
||||||
|
prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||||
|
lifetime = load("life"); tripA = load("a"); tripB = load("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void sample(EnergySnapshot s) {
|
||||||
|
long now = s.getTimestampMs();
|
||||||
|
Boolean parked = s.getParked();
|
||||||
|
if (Boolean.FALSE.equals(parked) && !Boolean.FALSE.equals(lastParked)) motor = zero();
|
||||||
|
if (lastMs > 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); }
|
||||||
|
}
|
||||||
@@ -46,15 +46,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/** Navigation destinations in page order. Parallel to {@link #panes}. */
|
/** Navigation destinations in page order. Parallel to {@link #panes}. */
|
||||||
private static final int[] TAB_IDS =
|
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 };
|
R.id.tabLog };
|
||||||
|
|
||||||
private static final int[] PAGE_TITLES = {
|
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
|
R.string.design_service_title, R.string.design_wifi_title, R.string.design_log_title
|
||||||
};
|
};
|
||||||
private static final int[] PAGE_SUBTITLES = {
|
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
|
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 servicePane;
|
||||||
private View logPane;
|
private View logPane;
|
||||||
private View vehiclePane;
|
private View vehiclePane;
|
||||||
|
private View consumptionPane;
|
||||||
|
private View chargingPane;
|
||||||
private View wifiPane;
|
private View wifiPane;
|
||||||
|
|
||||||
/** The five pages in navigation order. Parallel to {@link #TAB_IDS}. */
|
/** The five pages in navigation order. Parallel to {@link #TAB_IDS}. */
|
||||||
@@ -103,8 +105,18 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
*/
|
*/
|
||||||
private EnergyTelemetryReader vehicleReader;
|
private EnergyTelemetryReader vehicleReader;
|
||||||
private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer,
|
private TextView vehSoc, vehRange, vehPower, vehSpeed, vehOdometer,
|
||||||
vehCharging, vehExtTemp, vehHvac, vehSoe,
|
vehCharging, vehHvac, vehSoe,
|
||||||
vehTireFl, vehTireFr, vehTireRl, vehTireRr;
|
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
|
* 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();
|
refreshStatus();
|
||||||
refreshCallLog();
|
refreshCallLog();
|
||||||
refreshVehicle();
|
refreshVehicle();
|
||||||
|
refreshConsumption();
|
||||||
refreshWifi();
|
refreshWifi();
|
||||||
uiHandler.postDelayed(this, 2_000L);
|
uiHandler.postDelayed(this, 2_000L);
|
||||||
}
|
}
|
||||||
@@ -219,11 +232,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
abrpPane = inflatePane(R.layout.pane_abrp);
|
abrpPane = inflatePane(R.layout.pane_abrp);
|
||||||
servicePane = inflatePane(R.layout.pane_service);
|
servicePane = inflatePane(R.layout.pane_service);
|
||||||
vehiclePane = inflatePane(R.layout.pane_vehicle);
|
vehiclePane = inflatePane(R.layout.pane_vehicle);
|
||||||
|
consumptionPane = inflatePane(R.layout.pane_consumption);
|
||||||
|
chargingPane = inflatePane(R.layout.pane_charging);
|
||||||
wifiPane = inflatePane(R.layout.pane_wifi);
|
wifiPane = inflatePane(R.layout.pane_wifi);
|
||||||
logPane = inflatePane(R.layout.pane_log);
|
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();
|
bindVehiclePane();
|
||||||
|
bindConsumptionPane();
|
||||||
|
bindChargingPane();
|
||||||
bindWifiPane();
|
bindWifiPane();
|
||||||
|
|
||||||
apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout);
|
apiKeyLayout = abrpPane.findViewById(R.id.api_key_layout);
|
||||||
@@ -761,7 +778,6 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
vehSpeed = vehiclePane.findViewById(R.id.vehicle_speed);
|
vehSpeed = vehiclePane.findViewById(R.id.vehicle_speed);
|
||||||
vehOdometer = vehiclePane.findViewById(R.id.vehicle_odometer);
|
vehOdometer = vehiclePane.findViewById(R.id.vehicle_odometer);
|
||||||
vehCharging = vehiclePane.findViewById(R.id.vehicle_charging);
|
vehCharging = vehiclePane.findViewById(R.id.vehicle_charging);
|
||||||
vehExtTemp = vehiclePane.findViewById(R.id.vehicle_ext_temp);
|
|
||||||
vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac);
|
vehHvac = vehiclePane.findViewById(R.id.vehicle_hvac);
|
||||||
vehSoe = vehiclePane.findViewById(R.id.vehicle_soe);
|
vehSoe = vehiclePane.findViewById(R.id.vehicle_soe);
|
||||||
vehTireFl = vehiclePane.findViewById(R.id.vehicle_tire_fl);
|
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.
|
// The timestamp is passed explicitly: its Kotlin default is not visible from Java.
|
||||||
EnergySnapshot s = vehicleReader.read(System.currentTimeMillis());
|
EnergySnapshot s = vehicleReader.read(System.currentTimeMillis());
|
||||||
|
ConsumptionTracker.get(this).sample(s);
|
||||||
|
refreshCharging(s);
|
||||||
|
|
||||||
vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%"));
|
vehSoc.setText(fmt(s.getSocPercent(), "%.0f %%"));
|
||||||
vehRange.setText(fmt(s.getRangeKm(), "%.0f km"));
|
vehRange.setText(fmt(s.getRangeKm(), "%.0f km"));
|
||||||
vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW"));
|
vehPower.setText(fmt(s.getBatteryPowerKw(), "%.1f kW"));
|
||||||
vehSpeed.setText(fmt(s.getSpeedKmh(), "%.0f km/h"));
|
vehSpeed.setText(fmt(s.getSpeedKmh(), "%.0f km/h"));
|
||||||
vehOdometer.setText(fmt(s.getOdometerKm(), "%.0f km"));
|
vehOdometer.setText(fmt(s.getOdometerKm(), "%.0f km"));
|
||||||
vehExtTemp.setText(fmt(s.getOutsideTempCelsius(), "%.0f °C"));
|
|
||||||
vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
|
vehHvac.setText(fmt(s.getClimate().getDriverTargetCelsius(), "%.0f °C"));
|
||||||
vehSoe.setText(fmt(s.getBatteryEnergyKwh(), "%.1f kWh"));
|
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<String> 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.
|
* The charging row: the session state when the car reports one, the port otherwise.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ final class TelemetryPayload {
|
|||||||
|
|
||||||
// --- Lower priority ---
|
// --- Lower priority ---
|
||||||
Integer rangeKm; // km, vehicle's own estimate
|
Integer rangeKm; // km, vehicle's own estimate
|
||||||
Float extTemp; // °C, outside
|
|
||||||
Float cabinTemp; // °C
|
Float cabinTemp; // °C
|
||||||
Float battTempC; // °C, traction battery
|
Float battTempC; // °C, traction battery
|
||||||
Float capacityKwh; // kWh, usable pack capacity
|
Float capacityKwh; // kWh, usable pack capacity
|
||||||
@@ -63,13 +62,6 @@ final class TelemetryPayload {
|
|||||||
putIfPresent(tlm, "soc", soc);
|
putIfPresent(tlm, "soc", soc);
|
||||||
putIfPresent(tlm, "speed", speedKmh == null ? null : Math.round(speedKmh));
|
putIfPresent(tlm, "speed", speedKmh == null ? null : Math.round(speedKmh));
|
||||||
putIfPresent(tlm, "est_battery_range", rangeKm);
|
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));
|
if (powerKw != null) tlm.put("power", round2(powerKw));
|
||||||
putIfPresent(tlm, "is_charging", boolToInt(charging));
|
putIfPresent(tlm, "is_charging", boolToInt(charging));
|
||||||
putIfPresent(tlm, "is_dcfc", boolToInt(dcfc));
|
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)}.
|
* Used only to work out what was left out — see {@link #summarize(String)}.
|
||||||
*/
|
*/
|
||||||
private static final String[] ALL_FIELDS = {
|
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",
|
"batt_temp", "hvac_setpoint", "capacity", "soe", "kwh_charged", "odometer",
|
||||||
"is_charging", "is_dcfc", "is_parked", "lat", "lon", "elevation", "heading",
|
"is_charging", "is_dcfc", "is_parked", "lat", "lon", "elevation", "heading",
|
||||||
"tire_pressure_fl", "tire_pressure_fr", "tire_pressure_rl", "tire_pressure_rr",
|
"tire_pressure_fl", "tire_pressure_fr", "tire_pressure_rl", "tire_pressure_rr",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand" android:textAppearance="@style/Text.EV.Hero" android:textColor="@color/ev_accent" android:paddingStart="14dp" android:paddingTop="8dp" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand" android:textAppearance="@style/Text.EV.Hero" android:textColor="@color/ev_accent" android:paddingStart="14dp" android:paddingTop="8dp" />
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand_caption" android:textAppearance="@style/Text.EV.Secondary" android:paddingStart="14dp" android:layout_marginBottom="24dp" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_brand_caption" android:textAppearance="@style/Text.EV.Secondary" android:paddingStart="14dp" android:layout_marginBottom="24dp" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle" />
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabConsumption" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_consumption" app:icon="@drawable/ic_tab_vehicle" />
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_service" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabAbrp" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabAbrp" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service" />
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi" />
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginBottom="6dp" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi" />
|
||||||
|
|||||||
@@ -9,10 +9,6 @@
|
|||||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
||||||
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
|
||||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_ext_temp" />
|
|
||||||
<TextView android:id="@+id/vehicle_ext_temp" style="@style/DataValue" />
|
|
||||||
</TableRow>
|
|
||||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
||||||
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
<androidx.viewpager2.widget.ViewPager2 android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/>
|
<androidx.viewpager2.widget.ViewPager2 android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/>
|
||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp" android:background="@drawable/bg_sidebar">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp" android:background="@drawable/bg_sidebar">
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle"/>
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle"/>
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabConsumption" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_consumption" app:icon="@drawable/ic_tab_vehicle"/>
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_service"/>
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabAbrp" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp"/>
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabAbrp" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_abrp" app:icon="@drawable/ic_tab_abrp"/>
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service"/>
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabService" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_service" app:icon="@drawable/ic_tab_service"/>
|
||||||
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi"/>
|
<com.google.android.material.button.MaterialButton android:id="@+id/tabWifi" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_wifi" app:icon="@drawable/ic_tab_wifi"/>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true">
|
||||||
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp">
|
||||||
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:background="@drawable/bg_glass">
|
||||||
|
<TextView android:id="@+id/charge_status" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Şarj yok" android:textAppearance="@style/Text.EV.Title" android:textColor="@color/ev_cyan"/>
|
||||||
|
<GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="2" android:layout_marginTop="12dp">
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="SOC" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_soc" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya voltajı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_voltage" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Batarya akımı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_current" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj gücü" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_power" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Alınan enerji" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_energy" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="Şarj süresi" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/charge_duration" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="10dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
</GridLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:layout_marginTop="16dp" android:background="@drawable/bg_glass">
|
||||||
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="ŞARJ EĞRİSİ" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||||
|
<com.evsuite.abrp.ChargingGraphView android:id="@+id/charge_graph" android:layout_width="match_parent" android:layout_height="280dp" android:layout_marginTop="12dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true">
|
||||||
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp">
|
||||||
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="20dp" android:background="@drawable/bg_glass">
|
||||||
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="TÜKETİM DÖNEMİ" android:textAppearance="@style/Text.EV.Eyebrow"/>
|
||||||
|
<Spinner android:id="@+id/consumption_period" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginTop="8dp"/>
|
||||||
|
<GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:columnCount="2">
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Mesafe" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_distance" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Net enerji" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_energy" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Ortalama tüketim" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_average" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Ortalama hız" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_speed" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="Süre" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_time" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
<TextView android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="SOC farkı" android:textAppearance="@style/DataLabel"/><TextView android:id="@+id/consumption_soc" android:layout_width="0dp" android:layout_columnWeight="1" android:padding="12dp" android:text="—" android:textAppearance="@style/DataValue"/>
|
||||||
|
</GridLayout>
|
||||||
|
<com.google.android.material.button.MaterialButton android:id="@+id/consumption_reset" style="@style/Widget.EV.Button.Outlined" android:layout_width="match_parent" android:layout_height="64dp" android:layout_marginTop="12dp" android:text="Seçili tripi sıfırla" android:visibility="gone"/>
|
||||||
|
</LinearLayout>
|
||||||
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="Tüm zamanlar, Trip A ve Trip B cihazda kalıcı saklanır; uygulama ve OTA güncellemeleriyle silinmez." android:textAppearance="@style/Text.EV.Secondary"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
@@ -8,10 +8,6 @@
|
|||||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_odometer" />
|
||||||
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
<TextView android:id="@+id/vehicle_odometer" style="@style/DataValue" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
|
||||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_ext_temp" />
|
|
||||||
<TextView android:id="@+id/vehicle_ext_temp" style="@style/DataValue" />
|
|
||||||
</TableRow>
|
|
||||||
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
<TableRow style="@style/DataRow" android:minHeight="40dp" android:paddingTop="8dp" android:paddingBottom="8dp">
|
||||||
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
<TextView style="@style/DataLabel" android:text="@string/vehicle_row_hvac" />
|
||||||
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
<TextView android:id="@+id/vehicle_hvac" style="@style/DataValue" />
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
<string name="tab_abrp">ABRP</string>
|
<string name="tab_abrp">ABRP</string>
|
||||||
<string name="tab_service">Aktarım</string>
|
<string name="tab_service">Aktarım</string>
|
||||||
<string name="tab_vehicle">Genel</string>
|
<string name="tab_vehicle">Genel</string>
|
||||||
|
<string name="tab_consumption">Tüketim</string>
|
||||||
|
<string name="tab_charging">Şarj</string>
|
||||||
|
<string name="consumption_title">Tüketim</string>
|
||||||
|
<string name="consumption_subtitle">Başlangıç, motor, tüm zamanlar ve trip sayaçları</string>
|
||||||
|
<string name="charging_title">Şarj</string>
|
||||||
|
<string name="charging_subtitle">Canlı batarya değerleri ve SOC/güç eğrisi</string>
|
||||||
<string name="tab_wifi">Wi-Fi</string>
|
<string name="tab_wifi">Wi-Fi</string>
|
||||||
<string name="tab_log">Kayıtlar</string>
|
<string name="tab_log">Kayıtlar</string>
|
||||||
|
|
||||||
@@ -28,7 +34,6 @@
|
|||||||
<string name="vehicle_row_speed">Hız</string>
|
<string name="vehicle_row_speed">Hız</string>
|
||||||
<string name="vehicle_row_odometer">Kilometre</string>
|
<string name="vehicle_row_odometer">Kilometre</string>
|
||||||
<string name="vehicle_row_batt_temp">Batarya Sıcaklığı</string>
|
<string name="vehicle_row_batt_temp">Batarya Sıcaklığı</string>
|
||||||
<string name="vehicle_row_ext_temp">Dış Sıcaklık</string>
|
|
||||||
<string name="vehicle_row_cabin_temp">Kabin Sıcaklığı</string>
|
<string name="vehicle_row_cabin_temp">Kabin Sıcaklığı</string>
|
||||||
<string name="vehicle_row_hvac">Klima Ayarı</string>
|
<string name="vehicle_row_hvac">Klima Ayarı</string>
|
||||||
<string name="vehicle_row_capacity">Batarya Kapasitesi</string>
|
<string name="vehicle_row_capacity">Batarya Kapasitesi</string>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public class TelemetryPayloadTest {
|
|||||||
t.soc = 63;
|
t.soc = 63;
|
||||||
t.speedKmh = 48.4f;
|
t.speedKmh = 48.4f;
|
||||||
t.rangeKm = 210;
|
t.rangeKm = 210;
|
||||||
t.extTemp = 12f;
|
|
||||||
t.powerKw = -14.5f;
|
t.powerKw = -14.5f;
|
||||||
t.charging = false;
|
t.charging = false;
|
||||||
t.dcfc = false;
|
t.dcfc = false;
|
||||||
@@ -46,7 +45,7 @@ public class TelemetryPayloadTest {
|
|||||||
public void unreadablePropertiesAreOmittedNotZeroed() {
|
public void unreadablePropertiesAreOmittedNotZeroed() {
|
||||||
String json = noCar();
|
String json = noCar();
|
||||||
for (String field : new String[]{
|
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",
|
"is_charging", "is_dcfc", "is_parked", "cabin_temp",
|
||||||
"batt_temp", "capacity", "odometer", "hvac_setpoint",
|
"batt_temp", "capacity", "odometer", "hvac_setpoint",
|
||||||
"tire_pressure_fl", "tire_pressure_fr",
|
"tire_pressure_fl", "tire_pressure_fr",
|
||||||
@@ -84,7 +83,6 @@ public class TelemetryPayloadTest {
|
|||||||
assertEquals(63, json.getInt("soc"));
|
assertEquals(63, json.getInt("soc"));
|
||||||
assertEquals(48, json.getInt("speed")); // rounded
|
assertEquals(48, json.getInt("speed")); // rounded
|
||||||
assertEquals(210, json.getInt("est_battery_range"));
|
assertEquals(210, json.getInt("est_battery_range"));
|
||||||
assertEquals(12, json.getInt("ext_temp"));
|
|
||||||
assertEquals(-14.5, json.getDouble("power"), 0.001);
|
assertEquals(-14.5, json.getDouble("power"), 0.001);
|
||||||
assertEquals(0, json.getInt("is_charging"));
|
assertEquals(0, json.getInt("is_charging"));
|
||||||
assertEquals(0, json.getInt("is_dcfc"));
|
assertEquals(0, json.getInt("is_dcfc"));
|
||||||
@@ -296,27 +294,10 @@ public class TelemetryPayloadTest {
|
|||||||
new JSONObject(noCar());
|
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
|
@Test
|
||||||
public void summaryListsSentValuesAndOmittedFields() {
|
public void summaryListsSentValuesAndOmittedFields() {
|
||||||
String summary = TelemetryPayload.summarize(fullCar().build());
|
String summary = TelemetryPayload.summarize(fullCar().build());
|
||||||
assertTrue(summary, summary.contains("soc=63"));
|
assertTrue(summary, summary.contains("soc=63"));
|
||||||
assertTrue(summary, summary.contains("ext_temp=12"));
|
|
||||||
assertTrue(summary, summary.contains("cabin_temp=22"));
|
assertTrue(summary, summary.contains("cabin_temp=22"));
|
||||||
// Nothing gave us a position, so lat/lon must show up as omitted, not as 0.
|
// Nothing gave us a position, so lat/lon must show up as omitted, not as 0.
|
||||||
assertFalse(summary, summary.contains("lat="));
|
assertFalse(summary, summary.contains("lat="));
|
||||||
|
|||||||
Reference in New Issue
Block a user