Skip to content

Commit a2f9fa5

Browse files
malysclaude
andcommitted
feat(lights): exterior-light conditions and standstill actions (CR-046)
Tracks EVHardware 1.15.0 (200112f). - VehicleReader puts the low/high beam and fog states and switch positions. Absent stays absent; it never reads as OFF. - DirectExecutor gets branches SET_HEADLIGHTS / SET_HIGH_BEAM / SET_FOG_LIGHTS. They are gated (0 km/h only). ExteriorLights refuses low beams OFF and high beam forced ON. - Manifest + allowlist add CAR_EXTERIOR_LIGHTS and CONTROL_CAR_EXTERIOR_LIGHTS. - DiagnosticsTest: an unproven write on a generation that does not carry it reports the firmware reason first. The lights are proven on none yet. Every light entry stays hidden and inert until a capture proves a generation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f09da44 commit a2f9fa5

6 files changed

Lines changed: 29 additions & 2 deletions

File tree

‎.github/security/permission-allowlist.txt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ android.car.permission.CAR_ENERGY
1313
android.car.permission.CAR_POWERTRAIN
1414
android.car.permission.CAR_VENDOR_EXTENSION
1515
android.car.permission.CONTROL_CAR_CLIMATE
16+
# CR-046 (owner request 2026-10-08): light conditions read HEADLIGHTS/HIGH_BEAM/FOG_LIGHTS_STATE;
17+
# light actions write the matching _SWITCH only at 0 km/h (VehicleWriteGate), never low beams
18+
# OFF, never high beam forced ON (ExteriorLights). Both unused until a generation is proven.
19+
android.car.permission.CAR_EXTERIOR_LIGHTS
20+
android.car.permission.CONTROL_CAR_EXTERIOR_LIGHTS
1621
android.permission.WRITE_SETTINGS
1722

1823
# --- Own vehicle service ---

‎app/src/main/AndroidManifest.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
<uses-permission android:name="android.car.permission.CAR_POWERTRAIN" />
1717
<uses-permission android:name="android.car.permission.CAR_VENDOR_EXTENSION" />
1818
<uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE" />
19+
<!-- Exterior lights (CR-046): read for light conditions, control for the standstill
20+
actions. Signature-level, granted by the platform signature like the rest. -->
21+
<uses-permission android:name="android.car.permission.CAR_EXTERIOR_LIGHTS" />
22+
<uses-permission android:name="android.car.permission.CONTROL_CAR_EXTERIOR_LIGHTS" />
1923
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
2024

2125
<!-- Own vehicle service: foreground + wake + start at boot to catch the ignition. -->

‎app/src/main/java/com/evsuite/tasker/vehicle/DirectExecutor.kt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ class DirectExecutor(
172172
// Blocking for about a second: EVHardware reads the state three times before it
173173
// dares toggle it. The executor already runs off the main thread.
174174
ActionType.SET_ESC -> EVHardware.setEsc(b)
175+
// Exterior lights (CR-046) — gated; ExteriorLights refuses low-beam OFF / high-beam ON.
176+
ActionType.SET_HEADLIGHTS -> EVHardware.setHeadlightsSwitch(i)
177+
ActionType.SET_HIGH_BEAM -> EVHardware.setHighBeamSwitch(i)
178+
ActionType.SET_FOG_LIGHTS -> EVHardware.setFogLights(b)
175179
ActionType.SET_DROWSINESS -> EVHardware.setDrowsiness(b)
176180
ActionType.SET_DROWSINESS_SENSITIVITY -> EVHardware.setDrowsinessSensitivity(i)
177181
ActionType.SET_ACC_TJA_MODE -> EVHardware.setAccTjaMode(i)

‎app/src/main/java/com/evsuite/tasker/vehicle/VehicleReader.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ object VehicleReader {
107107
putIfReadable(SnapshotKeys.KEY_BRIGHTNESS, EVHardware.getScreenBrightnessPercent())
108108
}
109109

110+
// Exterior lights (CR-046): null until a generation is proven, so absent — never OFF.
111+
EVHardware.headlightsStateOrNull()?.let { put(SnapshotKeys.KEY_HEADLIGHTS, it) }
112+
EVHardware.highBeamOnOrNull()?.let { put(SnapshotKeys.KEY_HIGH_BEAM, it) }
113+
EVHardware.fogLightsOnOrNull()?.let { put(SnapshotKeys.KEY_FOG_LIGHTS, it) }
114+
EVHardware.headlightsSwitchOrNull()?.let { put(SnapshotKeys.KEY_HEADLIGHTS_SWITCH, it) }
115+
EVHardware.highBeamSwitchOrNull()?.let { put(SnapshotKeys.KEY_HIGH_BEAM_SWITCH, it) }
116+
EVHardware.fogLightsSwitchOnOrNull()?.let { put(SnapshotKeys.KEY_FOG_LIGHTS_SWITCH, it) }
117+
110118
// The `…OrNull` readers, not the Boolean ones: those answer false for a signal
111119
// the firmware never returned, which the engine would read as "the feature is
112120
// off" and act on. Absent from the snapshot is the honest answer.

‎app/src/test/java/com/evsuite/tasker/debug/DiagnosticsTest.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.evsuite.tasker.debug
22

33
import com.evsuite.hardware.FirmwareGen
4+
import com.evsuite.hardware.FirmwareSupport
45
import com.evsuite.hardware.catalog.ActionType
56
import com.evsuite.hardware.catalog.ConditionType
67
import com.evsuite.hardware.catalog.SnapshotKeys
@@ -421,11 +422,16 @@ class DiagnosticsTest {
421422

422423
val unproven = ActionType.entries.filter { !it.writeProven }.map { it.name }
423424
assertTrue("the glass is the case this exists for", "SET_WINDOWS" in unproven)
425+
// Every unproven write is blocked and hidden, whatever the reason given.
424426
unproven.forEach {
425-
assertEquals(Diagnostics.Reason.WRITE_UNPROVEN, entry(entries, it).reason)
426427
assertEquals(Diagnostics.Status.BLOCKED, entry(entries, it).status)
427428
assertTrue("$it must not be offered in the editor", entry(entries, it).hidden)
428429
}
430+
// The reason is WRITE_UNPROVEN where the firmware carries it. Where it does not — the
431+
// CR-046 lights, supported on no generation yet — the firmware verdict comes first.
432+
ActionType.entries
433+
.filter { !it.writeProven && FirmwareSupport.gensOf(it)?.contains(FirmwareGen.SWI68) != false }
434+
.forEach { assertEquals(Diagnostics.Reason.WRITE_UNPROVEN, entry(entries, it.name).reason) }
429435
assertTrue(
430436
"no OK action may be unproven",
431437
entries.filter { it.status == Diagnostics.Status.OK }.none { it.name in unproven }

0 commit comments

Comments
 (0)