Skip to content

Commit 6d6a90d

Browse files
malysclaude
andcommitted
feat(cycle): rules that read the clock wait for it at boot
The head unit boots on 2019-01-01 and the ignition cycle runs before the network or GPS corrects it. Rules with a time, weekday or date condition were then not evaluable for that ignition. They are now held back and run once the clock is set, polled every 2 s for up to five minutes; the other rules of the cycle run at once as before. Dropped if the ignition changes meanwhile, so a start-of-drive rule never fires after the drive; run anyway at the deadline, so the history still records CLOCK_UNSET. The wait is measured on elapsed time, since the wall clock is what jumps. The manual test does not wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 55a2e7e commit 6d6a90d

4 files changed

Lines changed: 97 additions & 9 deletions

File tree

‎app/src/main/java/com/evsuite/tasker/debug/Diagnostics.kt‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.evsuite.tasker.model.CompareOp
1212
import com.evsuite.tasker.model.Condition
1313
import com.evsuite.tasker.model.ConditionOutcome
1414
import com.evsuite.tasker.model.Snapshot
15+
import com.evsuite.tasker.util.WallClock
1516

1617
/**
1718
* Turns the execution context into a per-catalogue-entry verdict.
@@ -272,17 +273,11 @@ object Diagnostics {
272273
type in BLUETOOTH_CONDITIONS -> Reason.BLUETOOTH_OFF
273274
type == ConditionType.LOCATION_WITHIN -> Reason.NO_LOCATION
274275
type == ConditionType.WEATHER_NOW -> Reason.WEATHER_UNANSWERED
275-
!snapshot.clockSet && type in CLOCK_CONDITIONS -> Reason.CLOCK_UNSET
276+
!snapshot.clockSet && type in WallClock.CONDITIONS -> Reason.CLOCK_UNSET
276277
!snapshot.bridgeAvailable -> Reason.LAYER_NOT_READY
277278
else -> Reason.NOT_READABLE
278279
}
279280

280-
private val CLOCK_CONDITIONS = setOf(
281-
ConditionType.TIME_OF_DAY,
282-
ConditionType.DAY_OF_WEEK,
283-
ConditionType.DATE
284-
)
285-
286281
private val BLUETOOTH_CONDITIONS = setOf(
287282
ConditionType.BT_DEVICE_CONNECTED,
288283
ConditionType.ANY_BT_CONNECTED,

‎app/src/main/java/com/evsuite/tasker/util/WallClock.kt‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.evsuite.tasker.util
22

3+
import com.evsuite.hardware.catalog.ConditionType
4+
35
/**
46
* Whether the head unit's clock tells the time yet.
57
*
@@ -14,4 +16,7 @@ object WallClock {
1416
const val FLOOR_MS = 1_767_225_600_000L
1517

1618
fun isSet(nowMs: Long = System.currentTimeMillis()): Boolean = nowMs >= FLOOR_MS
19+
20+
/** The conditions that read this clock, and so cannot be answered before it is set. */
21+
val CONDITIONS = setOf(ConditionType.TIME_OF_DAY, ConditionType.DAY_OF_WEEK, ConditionType.DATE)
1722
}

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

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import com.evsuite.tasker.engine.RuleEngine
77
import com.evsuite.tasker.store.HistoryStore
88
import com.evsuite.tasker.store.RuleStore
99
import com.evsuite.tasker.model.EngineRun
10+
import com.evsuite.tasker.model.Rule
1011
import com.evsuite.tasker.util.BtDevices
1112
import com.evsuite.tasker.util.CarLocation
13+
import com.evsuite.tasker.util.WallClock
14+
import android.os.SystemClock
15+
import kotlin.concurrent.thread
1216

1317
/**
1418
* Bluetooth devices currently connected to the vehicle.
@@ -115,19 +119,29 @@ object RuleCycle {
115119
const val MANUAL = "MANUAL"
116120
const val PHYSICAL_BUTTON = "PHYSICAL_BUTTON"
117121

122+
/**
123+
* How long rules that read the clock wait for it after boot. The head unit corrects its
124+
* RTC default from the network or GPS; past this, the rules run anyway and the history
125+
* shows them not evaluable for `CLOCK_UNSET` rather than leaving no trace.
126+
*/
127+
private const val CLOCK_WAIT_MS = 5 * 60_000L
128+
private const val CLOCK_POLL_MS = 2_000L
129+
118130
// A P cycle can still be closing glass when ignition-off arrives. Keep whole cycles,
119131
// not just individual writes, atomic against every other trigger.
120132
@Synchronized
121133
fun run(
122134
context: Context,
123135
trigger: String,
124136
eventReadings: Map<String, Any> = emptyMap(),
125-
ruleId: String? = null
137+
ruleId: String? = null,
138+
/** The rules held back by [waitForClock], run once it is over. */
139+
clockWaited: Set<String>? = null
126140
) {
127141
val all = RuleStore(context).getAll()
128142
// Rules wired to a different event are not "skipped", they were never addressed: the
129143
// history would otherwise fill with a line per rule per ignition saying nothing.
130-
val rules = when (trigger) {
144+
var rules = when (trigger) {
131145
MANUAL -> all.filter { it.id == ruleId }
132146
PHYSICAL_BUTTON -> all.filter { it.hasPhysicalButtonCondition }
133147
// A position event belongs to the one rule whose place it is: its neighbours are
@@ -137,10 +151,20 @@ object RuleCycle {
137151
(ruleId == null || it.id == ruleId)
138152
}
139153
}
154+
if (clockWaited != null) rules = rules.filter { it.id in clockWaited }
140155
if (rules.isEmpty()) {
141156
AppLogger.i(TAG, "no rules for $trigger (${all.size} total) — skipped")
142157
return
143158
}
159+
// A manual test answers now, clock or not: the user is looking at the result.
160+
if (trigger != MANUAL && clockWaited == null && !WallClock.isSet()) {
161+
val (needClock, now) = rules.partition(::readsClock)
162+
if (needClock.isNotEmpty()) {
163+
waitForClock(context.applicationContext, trigger, eventReadings, needClock.map { it.id }.toSet())
164+
}
165+
rules = now
166+
if (rules.isEmpty()) return
167+
}
144168

145169
val startedAt = System.currentTimeMillis()
146170
val profileBridge = ProfileBridge(context).takeIf { it.connect() }
@@ -180,4 +204,40 @@ object RuleCycle {
180204
profileBridge?.disconnect()
181205
}
182206
}
207+
208+
/** Whether any case of [rule], "else if" ones included, asks the time, weekday or date. */
209+
internal fun readsClock(rule: Rule): Boolean =
210+
rule.branches.any { branch -> branch.conditions.any { it.type in WallClock.CONDITIONS } }
211+
212+
/**
213+
* Runs [ruleIds] again once the head unit's clock is set.
214+
*
215+
* At boot the clock reads 2019-01-01 until the network or GPS corrects it, and the
216+
* ignition cycle runs before that. Rules that read the time, the weekday or the date
217+
* wait for it; the others ran already and are not held up. Abandoned if the ignition
218+
* changes meanwhile: a start-of-drive rule must not fire after the drive is over. Waits
219+
* on elapsed time, because the wall clock is precisely what jumps.
220+
*/
221+
private fun waitForClock(
222+
context: Context,
223+
trigger: String,
224+
eventReadings: Map<String, Any>,
225+
ruleIds: Set<String>
226+
) {
227+
val ignition = EVHardware.getCurrentIgnitionState()
228+
AppLogger.i(TAG, "clock not set — ${ruleIds.size} $trigger rule(s) wait for it")
229+
thread(name = "mg4-tasker-clock-wait") {
230+
val start = SystemClock.elapsedRealtime()
231+
while (!WallClock.isSet() && SystemClock.elapsedRealtime() - start < CLOCK_WAIT_MS) {
232+
Thread.sleep(CLOCK_POLL_MS)
233+
}
234+
val waitedS = (SystemClock.elapsedRealtime() - start) / 1000
235+
if (EVHardware.getCurrentIgnitionState() != ignition) {
236+
AppLogger.i(TAG, "ignition changed during the ${waitedS} s clock wait — $trigger rules dropped")
237+
return@thread
238+
}
239+
AppLogger.i(TAG, "clock ${if (WallClock.isSet()) "set" else "still not set"} after ${waitedS} s — running held $trigger rules")
240+
run(context, trigger, eventReadings, clockWaited = ruleIds)
241+
}
242+
}
183243
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.evsuite.tasker.vehicle
2+
3+
import com.evsuite.hardware.catalog.ConditionType
4+
import com.evsuite.tasker.model.Branch
5+
import com.evsuite.tasker.model.Condition
6+
import com.evsuite.tasker.model.Rule
7+
import org.junit.Assert.assertFalse
8+
import org.junit.Assert.assertTrue
9+
import org.junit.Test
10+
11+
class RuleCycleClockTest {
12+
13+
@Test fun `a clock condition in any case holds the rule back`() {
14+
val outside = Condition(ConditionType.OUTSIDE_TEMP, number = 10f)
15+
assertFalse(RuleCycle.readsClock(Rule(name = "r", conditions = listOf(outside))))
16+
assertTrue(RuleCycle.readsClock(Rule(name = "r", conditions = listOf(Condition(ConditionType.DAY_OF_WEEK)))))
17+
// The "else if" is where it is easy to miss.
18+
assertTrue(
19+
RuleCycle.readsClock(
20+
Rule(
21+
name = "r",
22+
conditions = listOf(outside),
23+
elseIf = listOf(Branch(conditions = listOf(Condition(ConditionType.TIME_OF_DAY))))
24+
)
25+
)
26+
)
27+
}
28+
}

0 commit comments

Comments
 (0)