Skip to content

Commit ae24c99

Browse files
committed
feat(chargepilot): retain trip evidence and fix OTA
- keep retained trip averages within bounded history - trigger OTA from version double-tap instead of startup - fall back to API 28 archive signatures without weakening certificate matching
1 parent 4584742 commit ae24c99

19 files changed

Lines changed: 289 additions & 66 deletions

File tree

‎.codegraph/.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# CodeGraph data files — local to each machine, not for committing.
2+
# Ignore everything in .codegraph/ except this file itself, so transient
3+
# files (the database, daemon.pid, sockets, logs) never show up in git.
4+
*
5+
!.gitignore

‎AGENTS.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ workspace MIT default. EVHardware remains separately licensed.
5858
already list *except* primary emulated storage, and fall back only to this app's directory on
5959
the same volume. Never require positive proof that a volume is removable — this head unit
6060
cannot give it, and demanding it is why exports found nothing.
61-
- The unstable channel checks GitHub's rolling `unstable` pre-release at start and downloads a
62-
newer APK (`app/src/unstable/.../update/`). It is the second HTTP entry point and stays inside
61+
- The unstable channel checks GitHub's rolling `unstable` pre-release only after the driver
62+
double-taps the version on the Energy dashboard, then downloads a newer APK
63+
(`app/src/unstable/.../update/`). It is the second HTTP entry point and stays inside
6364
the same rules: `https` plus an exact-match host allowlist on the initial URL and every
6465
redirect hop, a size ceiling on the declared and the transferred length, and the archive only
6566
reaches shared storage after its signing certificate is proven identical to the running app's.

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.
66

77
### Added
88

9+
- **Unstable automatic history now keeps model-worthy drives.** Automatically detected trips are
10+
persisted only when their measured distance exceeds 5 km; manual recordings remain explicit and
11+
are always kept. Existing storage bounds remain unchanged: 200 summaries and 512 KiB, with old
12+
sample tracks evicted before trip summaries. At the same trip boundary, Q6 records the app's
13+
gross and regeneration-adjusted averages beside the vehicle's raw published average, so the
14+
next drive can establish why they diverge without assuming equivalent units or reset rules.
15+
16+
- Unstable trip-history artifacts now include `socModelFit`, computed from full stored tracks
17+
during export. CP-055 Q6 is recorded before validation artifact is written, so model evidence
18+
no longer depends on reopening Charging stop after a drive.
19+
920
- **The energy screen says whether this drive is a careful one.** A sixth readout tile reads
1021
*better than usual*, *usual* or *worse than usual* with the signed distance from the driver's
1122
own fitted expectation for this speed and this outside temperature — their history, not a
@@ -33,6 +44,11 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.
3344

3445
### Fixed
3546

47+
- **Finished-trip eco advice no longer drops persisted samples.** Trip tracks keep the first
48+
telemetry frame after each five-second boundary, commonly around 5.3 seconds apart. Replay used
49+
the stricter five-second live gap and discarded those intervals; it now uses the stored-track
50+
cadence while live monitoring keeps its original sleep-gap guard.
51+
3652
- **The advice line said nothing about having nothing to say.** With the switch on and no lever
3753
measurable — no fitted model yet, or too little movement in the window — the line simply was
3854
not drawn, which reads exactly like a switch that did nothing. It now states which it is, the

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ unique temporary file and atomic rename.
103103
Automatic detection is enabled by default. A foreground service owns the sampler so detection
104104
and recording continue when the driver opens another app. On firmware where speed stays
105105
unavailable, ten consecutive misses suspend idle background polling; opening the dashboard
106-
performs a bounded retry. Active trips remain fail-closed and under manual control. A trip's
107-
reported duration is the time actually covered by usable samples, not wall clock: a suspended
106+
performs a bounded retry. Active trips remain fail-closed and under manual control. The unstable
107+
build keeps automatically detected trips only above 5 km, concentrating the same bounded history
108+
budget on drives useful to the local consumption model. Manual recordings are always kept. A
109+
trip's reported duration is the time actually covered by usable samples, not wall clock: a suspended
108110
sampler adds nothing to duration, distance or energy, so consumption averages compare values
109111
measured over the same interval.
110112

‎analysis/CP-055_validation_drive.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ and never asks for anything.
8888

8989
## On arrival, parked
9090

91-
1. Open **Charging stop** once more and search the same destination again. This is what refits
92-
the consumption model with the drive you have just done, and that refit is question 6's
93-
answer.
91+
1. Open **Charging stop** once more and search the same destination again to exercise the screen
92+
path. Export independently refits the model from stored tracks and records question 6, so
93+
forgetting this tap no longer loses model evidence.
9494
2. Plug in the USB stick.
9595
3. **View diagnostics** → **Export to USB** → pick the stick. Wait for the confirmation naming
9696
the file.

‎app/src/main/java/com/evsuite/chargepilot/MainActivity.kt‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import android.graphics.Color
99
import android.graphics.drawable.ColorDrawable
1010
import android.os.Bundle
1111
import android.os.IBinder
12+
import android.view.GestureDetector
13+
import android.view.MotionEvent
1214
import android.view.View
1315
import android.widget.ImageView
1416
import android.widget.TextView
@@ -83,10 +85,6 @@ class MainActivity : PrimaryNavigationActivity() {
8385
outcome.filterValues { !it }.keys.forEach {
8486
AppLogger.w(TAG, "vehicle permission denied: $it")
8587
}
86-
// An activity holds one permission request at a time; a second launched while this one
87-
// is in flight is dropped by the framework. The unstable update check asks for storage
88-
// access of its own, so it starts here, once this dialog has been answered.
89-
UpdateHook.checkInBackground(this)
9088
}
9189

9290
/**
@@ -137,6 +135,23 @@ class MainActivity : PrimaryNavigationActivity() {
137135
binding.driftLine.setOnClickListener { forgetPlan() }
138136
binding.aboutAction.text = getString(R.string.about_version_badge, appVersion())
139137
binding.aboutAction.setOnClickListener { showAbout() }
138+
val versionGestures = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
139+
override fun onDown(event: MotionEvent): Boolean = true
140+
141+
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
142+
binding.aboutAction.performClick()
143+
return true
144+
}
145+
146+
override fun onDoubleTap(event: MotionEvent): Boolean {
147+
UpdateHook.checkInBackground(this@MainActivity)
148+
return true
149+
}
150+
})
151+
binding.aboutAction.setOnTouchListener { _, event ->
152+
versionGestures.onTouchEvent(event)
153+
true
154+
}
140155
renderUnavailable()
141156
requestVehiclePermissions()
142157
if (TripRecordingService.isAutomaticDetectionEnabled(this)) {
@@ -153,12 +168,7 @@ class MainActivity : PrimaryNavigationActivity() {
153168
val missing = VehiclePermissions.missing {
154169
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
155170
}
156-
// Unstable only, and nothing waits on it: the check runs on its own thread and speaks
157-
// only when a newer build is already downloaded. Stable contains no updater. It starts
158-
// here when there is no vehicle dialog to collide with, and from that dialog's answer
159-
// when there is.
160-
if (missing.isEmpty()) UpdateHook.checkInBackground(this)
161-
else vehiclePermissions.launch(missing)
171+
if (missing.isNotEmpty()) vehiclePermissions.launch(missing)
162172
}
163173

164174
/**
@@ -167,10 +177,6 @@ class MainActivity : PrimaryNavigationActivity() {
167177
*/
168178
override fun onStart() {
169179
super.onStart()
170-
// Unstable only, and a no-op unless the last check gave up without reaching GitHub:
171-
// the hook latches itself. This is what makes a head unit whose Wi-Fi arrived after
172-
// launch still find the update, without polling for it. Stable contains no updater.
173-
UpdateHook.checkInBackground(this)
174180
pack = VehicleSettings.read(this).pack
175181
loadRecentTrips()
176182
// The consumption fit reads the whole trip history, so it happens once per visit to

‎app/src/main/java/com/evsuite/chargepilot/TripRecordingService.kt‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,11 @@ class TripRecordingService : Service() {
217217
}
218218

219219
/** Called through the binder: the dashboard is on screen whenever a trip can be stopped. */
220-
fun stopTrip(onSaved: (() -> Unit)? = null) {
220+
fun stopTrip(onSaved: (() -> Unit)? = null) = finishTrip(false, onSaved)
221+
222+
private fun stopAutomaticTrip() = finishTrip(true, null)
223+
224+
private fun finishTrip(automaticallyDetected: Boolean, onSaved: (() -> Unit)?) {
221225
val endedAt = latestSnapshot?.timestampMs ?: System.currentTimeMillis()
222226
val recorded = EnergyTripSession.stop(endedAt)
223227
detector.reset()
@@ -229,6 +233,16 @@ class TripRecordingService : Service() {
229233
stopWhenNobodyNeedsIt()
230234
return
231235
}
236+
TripAverageEvidence.record(recorded.summary)
237+
if (!AutomaticTripRetention.shouldStore(automaticallyDetected, recorded.summary)) {
238+
AppLogger.i(
239+
TAG,
240+
"automatic trip not stored; distance_km=${recorded.summary.recordedDistanceKm}",
241+
)
242+
updateNotification()
243+
stopWhenNobodyNeedsIt()
244+
return
245+
}
232246
// Appending rewrites the history file, and the tap that triggers it arrives on the main
233247
// thread, so the write goes to the sampler's thread — which has nothing else to do
234248
// between two one-second reads. Shutting the service down waits for that write: this
@@ -304,7 +318,7 @@ class TripRecordingService : Service() {
304318
val result = detector.add(value)
305319
when (result.event) {
306320
TripDetector.Event.START -> beginTrip(value)
307-
TripDetector.Event.STOP -> stopTrip()
321+
TripDetector.Event.STOP -> stopAutomaticTrip()
308322
null -> Unit
309323
}
310324
if (result.event == null && result.state != previousState) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.evsuite.chargepilot
2+
3+
import com.evsuite.hardware.telemetry.EnergyTripSummary
4+
5+
/** Stable keeps the existing automatic-trip behaviour unchanged. */
6+
internal object AutomaticTripRetention {
7+
fun shouldStore(
8+
@Suppress("UNUSED_PARAMETER") automaticallyDetected: Boolean,
9+
@Suppress("UNUSED_PARAMETER") summary: EnergyTripSummary,
10+
): Boolean = true
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.evsuite.chargepilot
2+
3+
import com.evsuite.hardware.telemetry.EnergyTripSummary
4+
5+
/** Stable records no development evidence. */
6+
internal object TripAverageEvidence {
7+
fun record(@Suppress("UNUSED_PARAMETER") summary: EnergyTripSummary) = Unit
8+
}

0 commit comments

Comments
 (0)