Skip to content

Commit 3a933de

Browse files
malysclaude
andcommitted
feat(diagnostic): log read-backs around regen, Custom and climate writes
The parked on-vehicle tests for RI-006, CR-040 and CR-041 need a read before and after each write, and today the log carries only the write and its boolean result, so a test leaves no evidence of what the car did with it. ReadBack logs [READBACK] INFO lines (EV_HW, EV_CUSTOM, EV_PROFILE tags) from the existing readers: mapped regen level, CustomDrive positions and every climate reader. Reads only. No write, value, order, delay or lock changes. In ProfileApplier the reads sit before a block's first write and after its last one, never between two writes; no wait was added there, so the "after" lines are taken right after the existing final step. The dashboard waits 500 ms (regen) and 300 ms (Custom) after its single write, off the UI. A null or throwing read becomes text, and a call on the main thread (the deferred Katman4 path) logs that it skipped. EVHardware publishes no one-pedal reader and getRegenLevel() maps unknown codes to MEDIUM; both are stated in the log line rather than worked around in app code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 80bf4ce commit 3a933de

5 files changed

Lines changed: 94 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ All notable changes to this project are documented here. Format follows
1111
- **Custom drive mode settings (CR-040).** When a profile selects Custom, the editor offers power response (Eco / Normal / Sport), steering and brake-pedal feel (Comfort / Normal / Sport). They are written after the drive mode, only when the profile's mode is Custom, 150 ms apart and behind the standstill gate. On the dashboard, tapping Custom while the car is already in Custom opens the same three settings live. Available on SWI68, SWI165, SWI69, SWI131 and SWI132; not on SWI133. The value scales beyond the read/write codes recorded in RI-006 still need an on-vehicle confirmation.
1212
- **Climate in profiles (CR-041).** An optional climate block on the comfort tab, unchecked by default: power, setpoint, AUTO, fan, A/C, recirculation and the two defrosters, the last three with an "Unchanged" choice. Climate off writes nothing else, and the fan is written only in manual mode, so a profile never knocks the unit out of AUTO. Climate is a comfort write, like seat heating, and is not speed-gated. Not offered on SWI133, which has no climate write path.
1313
- Profiles saved before this release configure neither feature and behave exactly as before.
14+
- **Read-back logging for the parked on-vehicle tests (RI-006, CR-040, CR-041).** `[READBACK]` INFO lines in the log console and diagnostic export record the car's state before and after the dashboard's regen and Custom writes (500 ms and 300 ms after), around a profile's drive/regen block, its Custom block and its climate plan. Reads only: no write, value, order, delay or lock changed, and a failed read is logged as text. One-pedal has no reader in EVHardware, so it is logged as such.
1415

1516
## [3.0.4] - 2026-09-22
1617

‎app/src/main/java/com/evsuite/profile/profile/ProfileApplier.kt‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ object ProfileApplier {
9696
val roadWrites = VehicleWriteGate.allow("profile '${profile.name}' — driving settings")
9797

9898
if (roadWrites) {
99+
// RI-006 read-back: before the block's first write, never between its writes.
100+
ReadBack.regen("ProfileApplier", "before")
99101
// Mode de conduite (rapide — binder call)
100102
val dmOk = EVHardware.setDriveMode(profile.driveMode)
101103
AppLogger.i(TAG, " DriveMode=${profile.driveMode.label} → $dmOk")
@@ -123,6 +125,10 @@ object ProfileApplier {
123125
// Climate (CR-041) — comfort, like the heating above: not speed-gated.
124126
profile.climate?.let { applyClimate(it) }
125127

128+
// RI-006 read-back, after the Katman1 block's last write. No wait added: there is
129+
// no existing delay after it, and a new one here would hold up every profile.
130+
if (roadWrites) ReadBack.regen("ProfileApplier", "after Katman1 (no added wait)")
131+
126132
AppLogger.i(TAG, "Profile '${profile.name}' Katman1 completed — ok=$ok")
127133
onComplete?.invoke(ok)
128134

@@ -268,6 +274,9 @@ object ProfileApplier {
268274
* and stays where the car has it.
269275
*/
270276
private fun applyCustomDrive(profile: DrivingProfile) {
277+
// CR-040 read-back: a snapshot before the first write and after the last write's own
278+
// 150 ms — never between two writes, so the existing spacing stays exactly as it is.
279+
ReadBack.custom("ProfileApplier", "before")
271280
listOf(
272281
CustomDrive.Setting.POWER to profile.customPower,
273282
CustomDrive.Setting.STEERING to profile.customSteering,
@@ -282,6 +291,7 @@ object ProfileApplier {
282291
AppLogger.i(TAG, " Custom $setting=$index → $done")
283292
try { Thread.sleep(150) } catch (_: InterruptedException) {}
284293
}
294+
ReadBack.custom("ProfileApplier", "after")
285295
}
286296

287297
/** Runs [ClimatePlan]'s steps, spaced like every other multi-write sequence here. */
@@ -290,10 +300,13 @@ object ProfileApplier {
290300
AppLogger.w(TAG, " Climate skipped — climate service not available")
291301
return
292302
}
303+
ReadBack.climate("before") // CR-041 read-back
293304
ClimatePlan.steps(climate).forEachIndexed { i, step ->
294305
if (i > 0) try { Thread.sleep(150) } catch (_: InterruptedException) {}
295306
AppLogger.i(TAG, " Climate ${step.control}=${step.value} → ${ClimatePlan.write(step)}")
296307
}
308+
// After the plan's last write; the spec's ~2 s wait is not added (it would be a new delay).
309+
ReadBack.climate("after (no added wait)")
297310
}
298311

299312
/**
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.evsuite.profile.profile
2+
3+
import android.os.Looper
4+
import com.evsuite.hardware.AppLogger
5+
import com.evsuite.hardware.CustomDrive
6+
import com.evsuite.hardware.EVHardware
7+
import com.evsuite.hardware.saic.SaicClimate
8+
9+
/**
10+
* Read-back lines for the owner's parked tests (RI-006, CR-040, CR-041): the car's state just
11+
* before and after a write, so the log carries a read/write/read triple per step.
12+
*
13+
* Reads only. Nothing here writes, waits, or throws: a failed or null read is a word in the
14+
* line, and a call on the main thread logs that it skipped instead of blocking on the binder.
15+
*/
16+
object ReadBack {
17+
18+
/** RI-006. EVHardware publishes no one-pedal reader, and [EVHardware.getRegenLevel] maps an
19+
* unknown code to MEDIUM — so this is the mapped level, not the raw int. */
20+
fun regen(where: String, phase: String) = line("EV_HW", where, phase) {
21+
"regen=${show { EVHardware.getRegenLevel()?.let { "${it.name}(${it.value})" } }} onePedal=no reader"
22+
}
23+
24+
/** CR-040. [CustomDrive.read] answers the position index; null is unreadable or off-scale. */
25+
fun custom(where: String, phase: String, settings: List<CustomDrive.Setting> = CustomDrive.Setting.values().toList()) =
26+
line("EV_CUSTOM", where, phase) { settings.joinToString(" ") { "$it=${show { CustomDrive.read(it) }}" } }
27+
28+
/** CR-041. Every climate reader the plan writes. */
29+
fun climate(phase: String) = line("EV_PROFILE", "Climate", phase) {
30+
"power=${show { SaicClimate.powerOn() }} auto=${show { SaicClimate.autoOn() }} " +
31+
"ac=${show { SaicClimate.acOn() }} temp=${show { SaicClimate.driverTemp() }} " +
32+
"fan=${show { SaicClimate.fanLevel() }} recirc=${show { SaicClimate.recirculationOn() }} " +
33+
"frontDefrost=${show { SaicClimate.frontDefrostOn() }} rearDefrost=${show { SaicClimate.rearDefrostOn() }}"
34+
}
35+
36+
/** [read]'s value as text; null and exceptions become words, never a throw. */
37+
internal fun show(read: () -> Any?): String =
38+
try { read()?.toString() ?: "unreadable" } catch (e: Exception) { "exc(${e.javaClass.simpleName})" }
39+
40+
private fun line(tag: String, where: String, phase: String, body: () -> String) {
41+
try {
42+
val text = if (Looper.myLooper() == Looper.getMainLooper()) "skipped — main thread" else body()
43+
AppLogger.i(tag, " [READBACK] $where $phase: $text")
44+
} catch (_: Exception) {}
45+
}
46+
}

‎app/src/main/java/com/evsuite/profile/ui/DashboardFragment.kt‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import com.evsuite.hardware.EVHardware.Swi68Mode
2323
import com.evsuite.hardware.model.DriveMode
2424
import com.evsuite.hardware.model.RegenLevel
2525
import com.evsuite.hardware.FirmwareInfo
26+
import com.evsuite.profile.profile.ReadBack
2627
import kotlinx.coroutines.CoroutineScope
2728
import kotlinx.coroutines.Dispatchers
29+
import kotlinx.coroutines.delay
2830
import kotlinx.coroutines.launch
2931
import kotlinx.coroutines.withContext
3032

@@ -279,7 +281,13 @@ class DashboardFragment : Fragment() {
279281
regenButtons.forEach { (level, btn) ->
280282
btn.setOnClickListener {
281283
applyRegenUI(level)
282-
CoroutineScope(Dispatchers.IO).launch { EVHardware.setRegenLevel(level) }
284+
CoroutineScope(Dispatchers.IO).launch {
285+
// RI-006 read-back around the one write; the wait comes after it, never before.
286+
ReadBack.regen("Dashboard", "before ${level.name}")
287+
EVHardware.setRegenLevel(level)
288+
delay(500)
289+
ReadBack.regen("Dashboard", "500 ms after ${level.name}")
290+
}
283291
}
284292
}
285293

@@ -663,8 +671,12 @@ class DashboardFragment : Fragment() {
663671
b.alpha = if (available) 1f else 0.35f
664672
b.setOnClickListener {
665673
CoroutineScope(Dispatchers.IO).launch {
674+
// CR-040 read-back; the wait follows the highlight, so the UI is not held.
675+
ReadBack.custom("Dashboard", "before $index", listOf(setting))
666676
val ok = CustomDrive.write(setting, index)
667677
withContext(Dispatchers.Main) { if (ok && isAdded) highlight(index) }
678+
delay(300)
679+
ReadBack.custom("Dashboard", "300 ms after $index", listOf(setting))
668680
}
669681
}
670682
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.evsuite.profile.profile
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Test
5+
6+
/** A read-back sits inside a write sequence: a failed read must become text, never a throw. */
7+
class ReadBackTest {
8+
9+
@Test fun valueNullAndExceptionAreAllText() {
10+
assertEquals("2", ReadBack.show { 2 })
11+
assertEquals("unreadable", ReadBack.show { null })
12+
assertEquals("exc(IllegalStateException)", ReadBack.show { throw IllegalStateException("binder") })
13+
}
14+
15+
@Test fun lineNeverThrowsOffDevice() {
16+
// No car and no CarPropertyManager in a JVM test: every reader fails, nothing escapes.
17+
ReadBack.regen("test", "before")
18+
ReadBack.custom("test", "before")
19+
ReadBack.climate("before")
20+
}
21+
}

0 commit comments

Comments
 (0)