Skip to content

Commit 26b3d45

Browse files
malysclaude
andcommitted
fix(speed): stop converting a speed the vehicle already reports in km/h
AAOS specifies PERF_VEHICLE_SPEED in metres per second, so every read was multiplied by 3.6. On SWI68 the property already reports km/h, and the second conversion scaled every distance integrated from it by the same factor. The evidence is a bundle from SWI68-29958-1300R67. A 2.4 km drive was recorded as 8.46 km, and a second run of the same route as 7.69 km — 124 and 129 km/h average over four minutes of town driving. Divided by 3.6 those trips read 2.35 km at 34 km/h, which is the drive that happened. The navigation adapter's odometer reads on this firmware and gives the independent reference the vehicle property could not. VehicleSpeedScale makes the conversion evidence-gated per generation, the way every other firmware-dependent read here already is. SWI68 is listed because a vehicle showed it. Every other generation keeps the specified conversion: unproven, but wrong in the conservative direction, because a speed read too high makes VehicleWriteGate refuse a write more often rather than less. Refs: CP-003 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3df0198 commit 26b3d45

4 files changed

Lines changed: 110 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@ All notable changes to this project are documented here. Format follows
88

99
### Fixed
1010

11+
- **`PERF_VEHICLE_SPEED` already reports km/h on SWI68, and was being converted a second time.**
12+
AAOS specifies the property in metres per second, so every read was multiplied by 3.6 — and
13+
every distance integrated from it was scaled by the same factor. A 2.4 km drive was recorded
14+
as 8.46 km and, on a second run, 7.69 km: 124 and 129 km/h average over four minutes of town
15+
driving. Divided by 3.6 the same trips read 2.35 km at 34 km/h, which is the drive that
16+
happened. `VehicleSpeedScale` makes the conversion evidence-gated per generation like every
17+
other firmware-dependent read here. SWI68 is listed on the 2026-09-04 bundle from
18+
SWI68-29958-1300R67; every other generation keeps the specified conversion, unproven but wrong
19+
in the conservative direction, since a speed read too high makes `VehicleWriteGate` refuse
20+
more often rather than less.
21+
22+
### Fixed
23+
1124
- **Repeated vehicle states no longer flood the diagnostic log.** The ignition condition
1225
arrives about ten times a second and the gear is read once a second; both were logged every
1326
time, so a 400-line retained log held under a minute of history and anything older had already

‎lib/src/main/java/com/evsuite/hardware/EVHardware.kt‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,14 +368,17 @@ object EVHardware {
368368
/**
369369
* Vehicle speed in km/h, or null if it cannot be read (CPM not ready,
370370
* property non supportsde, exception). [VehicleWriteGate] traite null comme un refus.
371+
*
372+
* The property's unit varies by generation — see [VehicleSpeedScale].
371373
*/
372374
fun getVehicleSpeedKmh(): Float? {
373-
val mps = getFloatPropertyCPM(PROP_VEHICLE_SPEED, AREA_GLOBAL)
375+
val raw = getFloatPropertyCPM(PROP_VEHICLE_SPEED, AREA_GLOBAL)
374376
?: getFloatPropertyCPM(PROP_VEHICLE_SPEED, 0)
375377
?: return null
376-
// PERF_VEHICLE_SPEED is signed (negative in reverse): it is the speed
377-
// absolue qui compte pour savoir si le vehicle bouge.
378-
return kotlin.math.abs(mps) * 3.6f
378+
// The unit is firmware-dependent, not what AAOS specifies: SWI68 already reports
379+
// km/h. [VehicleSpeedScale] carries the evidence and takes the magnitude, since the
380+
// property is signed in reverse and every consumer asks whether the car is moving.
381+
return VehicleSpeedScale.toKmh(raw, FirmwareInfo.getGeneration())
379382
}
380383

381384
/**
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.evsuite.hardware
2+
3+
import kotlin.math.abs
4+
5+
/**
6+
* What `PERF_VEHICLE_SPEED` is actually in, per firmware generation.
7+
*
8+
* AAOS defines the property as metres per second, so every read was multiplied by 3.6. On
9+
* SWI68 that is wrong: the property already reports km/h, and multiplying a second time
10+
* scaled every derived distance by the same factor. A 2.4 km drive was recorded twice as
11+
* 8.46 km and 7.69 km — 124 and 129 km/h average over four minutes of town driving — and
12+
* dividing by 3.6 gives 2.35 km at 34 km/h, which is the drive that happened.
13+
*
14+
* The conversion is therefore evidence-gated like every other firmware-dependent read here.
15+
* A generation is listed only once a vehicle has shown it, and an unlisted generation keeps
16+
* the specified conversion: unproven, but wrong in the conservative direction, because a
17+
* speed read too high makes [VehicleWriteGate] refuse more often rather than less.
18+
*/
19+
object VehicleSpeedScale {
20+
21+
/** Metres per second to km/h, for generations that really do report the specified unit. */
22+
private const val MPS_TO_KMH = 3.6f
23+
24+
/**
25+
* Generations proven on a vehicle to report km/h directly from `PERF_VEHICLE_SPEED`.
26+
*
27+
* SWI68: bundle of 2026-09-04, firmware SWI68-29958-1300R67. Two recorded trips over a
28+
* route of known length, both over by the same 3.5x, with the adapter odometer as the
29+
* independent reference. Add a generation here only with the same kind of evidence.
30+
*/
31+
private val REPORTS_KMH = setOf(FirmwareInfo.Gen.SWI68)
32+
33+
/** True when this generation needs no conversion at all. */
34+
fun reportsKmh(generation: FirmwareInfo.Gen): Boolean = generation in REPORTS_KMH
35+
36+
/**
37+
* The raw property value as km/h.
38+
*
39+
* The property is signed — negative in reverse — and every consumer here asks whether the
40+
* car is moving, so the magnitude is what comes back.
41+
*/
42+
fun toKmh(raw: Float, generation: FirmwareInfo.Gen): Float =
43+
if (reportsKmh(generation)) abs(raw) else abs(raw) * MPS_TO_KMH
44+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.evsuite.hardware
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Assert.assertFalse
5+
import org.junit.Assert.assertTrue
6+
import org.junit.Test
7+
8+
class VehicleSpeedScaleTest {
9+
10+
@Test fun `SWI68 reports km per hour and is not converted again`() {
11+
assertTrue(VehicleSpeedScale.reportsKmh(FirmwareInfo.Gen.SWI68))
12+
assertEquals(34.4f, VehicleSpeedScale.toKmh(34.4f, FirmwareInfo.Gen.SWI68), 0.001f)
13+
}
14+
15+
@Test fun `an unproven generation keeps the specified conversion`() {
16+
FirmwareInfo.Gen.entries
17+
.filterNot { it == FirmwareInfo.Gen.SWI68 }
18+
.forEach { generation ->
19+
assertFalse("$generation is not proven", VehicleSpeedScale.reportsKmh(generation))
20+
assertEquals(
21+
"$generation must keep the AAOS conversion",
22+
36.0f,
23+
VehicleSpeedScale.toKmh(10.0f, generation),
24+
0.001f,
25+
)
26+
}
27+
}
28+
29+
@Test fun `reverse is a speed, not a negative one`() {
30+
assertEquals(12.0f, VehicleSpeedScale.toKmh(-12.0f, FirmwareInfo.Gen.SWI68), 0.001f)
31+
assertEquals(36.0f, VehicleSpeedScale.toKmh(-10.0f, FirmwareInfo.Gen.SWI69), 0.001f)
32+
}
33+
34+
@Test fun `the drive that proved it now integrates to the route it followed`() {
35+
// The recorded trip: 245836 ms at a steady 34.4 km/h reads as 2.35 km, not 8.46 km.
36+
val hours = 245_836L / 3_600_000.0
37+
val kmh = VehicleSpeedScale.toKmh(34.4f, FirmwareInfo.Gen.SWI68)
38+
assertEquals(2.35, kmh * hours, 0.02)
39+
}
40+
41+
@Test fun `standstill stays standstill in every generation`() {
42+
FirmwareInfo.Gen.entries.forEach {
43+
assertEquals(0.0f, VehicleSpeedScale.toKmh(0.0f, it), 0.0f)
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)