sessiond: debounce the proximity edge that actually flaps
Measured: 44 near-episodes in 2.4h, median dwell 1s, 15 sub-second. The negative debounce §9.5 specified would have delayed each blip, not removed it.
This commit is contained in:
parent
813010dbb6
commit
4cd7ddd046
1 changed files with 327 additions and 29 deletions
|
|
@ -71,6 +71,33 @@ pub const EVIDENCE_TTL: Duration = Duration::from_secs(30);
|
||||||
/// periodically, so silence means silence rather than "nothing changed".
|
/// periodically, so silence means silence rather than "nothing changed".
|
||||||
pub const SOURCE_DOWN_AFTER: Duration = Duration::from_secs(90);
|
pub const SOURCE_DOWN_AFTER: Duration = Duration::from_secs(90);
|
||||||
|
|
||||||
|
/// How long proximity must read `near` before the machine believes it.
|
||||||
|
///
|
||||||
|
/// §9.5 specified Android's `DisplayPowerProximityStateController` — 0 ms
|
||||||
|
/// positive, 250 ms negative — and that is the wrong shape for this device.
|
||||||
|
/// Measured on the trail 2026-07-26: 44 near-episodes over 2.4 hours, median
|
||||||
|
/// dwell **1 second**, 15 of them sub-second, and a median 115 s of quiet
|
||||||
|
/// between them. That is not a sensor bouncing around a threshold; it is
|
||||||
|
/// isolated one-second blips. A negative debounce delays believing `far`, so it
|
||||||
|
/// would have turned each 1 s blip into a 1.25 s blip and left all 88
|
||||||
|
/// transitions in place.
|
||||||
|
///
|
||||||
|
/// Android debounces the negative edge because there `near` means *screen off
|
||||||
|
/// at the ear, immediately* — a positive delay would be felt. That constraint
|
||||||
|
/// left when proximity stopped actuating: it now only vetoes tap-to-wake, where
|
||||||
|
/// waiting is imperceptible and a false veto is the more annoying failure.
|
||||||
|
///
|
||||||
|
/// Nine of the 44 episodes ran ≥5 s (max 173 s). Those are the real ones —
|
||||||
|
/// pocket, ear, deliberate cover — and they survive this threshold untouched.
|
||||||
|
pub const PROXIMITY_NEAR_DEBOUNCE: Duration = Duration::from_millis(700);
|
||||||
|
|
||||||
|
/// How long proximity must read `far` before the machine believes it.
|
||||||
|
///
|
||||||
|
/// Zero. An uncovered sensor is believed at once: the reading that ends a veto
|
||||||
|
/// should never be the slow one, and erring toward `far` errs toward letting a
|
||||||
|
/// wake through, which is the recoverable direction.
|
||||||
|
pub const PROXIMITY_FAR_DEBOUNCE: Duration = Duration::ZERO;
|
||||||
|
|
||||||
/// How long a blank waits for the compositor to acknowledge the lock it asked
|
/// How long a blank waits for the compositor to acknowledge the lock it asked
|
||||||
/// for before going dark anyway.
|
/// for before going dark anyway.
|
||||||
///
|
///
|
||||||
|
|
@ -119,6 +146,10 @@ pub struct DeviceStatePolicy {
|
||||||
pub unlocked_blank_after: Option<Duration>,
|
pub unlocked_blank_after: Option<Duration>,
|
||||||
/// A source silent for this long, having previously reported, is down.
|
/// A source silent for this long, having previously reported, is down.
|
||||||
pub source_down_after: Duration,
|
pub source_down_after: Duration,
|
||||||
|
/// How long proximity must hold `near` before the machine believes it.
|
||||||
|
pub proximity_near_debounce: Duration,
|
||||||
|
/// How long proximity must hold `far` before the machine believes it.
|
||||||
|
pub proximity_far_debounce: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeviceStatePolicy {
|
impl DeviceStatePolicy {
|
||||||
|
|
@ -200,6 +231,8 @@ impl Default for DeviceStatePolicy {
|
||||||
lock_ack_budget: LOCK_ACK_BUDGET,
|
lock_ack_budget: LOCK_ACK_BUDGET,
|
||||||
unlocked_blank_after: None,
|
unlocked_blank_after: None,
|
||||||
source_down_after: SOURCE_DOWN_AFTER,
|
source_down_after: SOURCE_DOWN_AFTER,
|
||||||
|
proximity_near_debounce: PROXIMITY_NEAR_DEBOUNCE,
|
||||||
|
proximity_far_debounce: PROXIMITY_FAR_DEBOUNCE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -915,6 +948,11 @@ pub struct DeviceStateMachine {
|
||||||
/// deadline after which the panel goes dark regardless — see
|
/// deadline after which the panel goes dark regardless — see
|
||||||
/// `LOCK_ACK_BUDGET` for why that fail-open is the documented choice.
|
/// `LOCK_ACK_BUDGET` for why that fail-open is the documented choice.
|
||||||
pending_blank: Option<Instant>,
|
pending_blank: Option<Instant>,
|
||||||
|
/// The last raw proximity reading, before debounce. `sensor_evidence`
|
||||||
|
/// carries the believed value; this carries what the sensor actually said.
|
||||||
|
proximity_raw: bool,
|
||||||
|
/// When the raw reading last changed. The debounce measures from here.
|
||||||
|
proximity_since: Option<Instant>,
|
||||||
/// Timed policy. Settable, so Settings can own it.
|
/// Timed policy. Settable, so Settings can own it.
|
||||||
pub policy: DeviceStatePolicy,
|
pub policy: DeviceStatePolicy,
|
||||||
}
|
}
|
||||||
|
|
@ -981,6 +1019,8 @@ impl DeviceStateMachine {
|
||||||
blank_requested: false,
|
blank_requested: false,
|
||||||
dimmed: false,
|
dimmed: false,
|
||||||
pending_blank: None,
|
pending_blank: None,
|
||||||
|
proximity_raw: false,
|
||||||
|
proximity_since: None,
|
||||||
// Loaded, not defaulted: the user's Auto-Lock choices are settings,
|
// Loaded, not defaulted: the user's Auto-Lock choices are settings,
|
||||||
// and a setting that reverts on reboot is not a setting.
|
// and a setting that reverts on reboot is not a setting.
|
||||||
policy: DeviceStatePolicy::load(),
|
policy: DeviceStatePolicy::load(),
|
||||||
|
|
@ -1018,7 +1058,11 @@ impl DeviceStateMachine {
|
||||||
/// `tick` with the clock passed in, so the rules can be tested at any
|
/// `tick` with the clock passed in, so the rules can be tested at any
|
||||||
/// point on the timeline instead of by sleeping.
|
/// point on the timeline instead of by sleeping.
|
||||||
pub fn tick_at(&mut self, now: Instant) -> Vec<Action> {
|
pub fn tick_at(&mut self, now: Instant) -> Vec<Action> {
|
||||||
|
// Expiry first. A source that has gone stale must not be allowed to
|
||||||
|
// win a pending debounce and then be cleared in the same tick — that
|
||||||
|
// would put a transition in the trail for a reading nobody confirmed.
|
||||||
self.expire_stale_evidence(now);
|
self.expire_stale_evidence(now);
|
||||||
|
self.resolve_proximity_debounce(now);
|
||||||
self.evaluate_source_health(now);
|
self.evaluate_source_health(now);
|
||||||
|
|
||||||
let mut actions = Vec::new();
|
let mut actions = Vec::new();
|
||||||
|
|
@ -1212,6 +1256,12 @@ impl DeviceStateMachine {
|
||||||
&mut self.evidence_seen.proximity,
|
&mut self.evidence_seen.proximity,
|
||||||
&mut self.sensor_evidence.proximity_near,
|
&mut self.sensor_evidence.proximity_near,
|
||||||
) {
|
) {
|
||||||
|
// Clear the raw reading and any pending debounce with it. Stale
|
||||||
|
// means unknown, and a half-resolved edge left behind an expiry
|
||||||
|
// would let a reading nobody has confirmed in 30 s win the moment
|
||||||
|
// the next tick ran.
|
||||||
|
self.proximity_raw = false;
|
||||||
|
self.proximity_since = None;
|
||||||
dropped.push("proximity");
|
dropped.push("proximity");
|
||||||
}
|
}
|
||||||
if expired(
|
if expired(
|
||||||
|
|
@ -1598,6 +1648,87 @@ impl DeviceStateMachine {
|
||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decide whether to believe a raw proximity reading yet.
|
||||||
|
///
|
||||||
|
/// Returns the *believed* value. A raw reading that disagrees with the
|
||||||
|
/// believed one starts a clock; it only wins once it has held for the
|
||||||
|
/// direction's threshold. `resolve_proximity_debounce` finishes the job on
|
||||||
|
/// the tick, for the case where the sensor reports once and goes quiet.
|
||||||
|
fn debounce_proximity(&mut self, raw: bool, now: Instant) -> bool {
|
||||||
|
if raw != self.proximity_raw {
|
||||||
|
self.proximity_raw = raw;
|
||||||
|
self.proximity_since = Some(now);
|
||||||
|
}
|
||||||
|
let believed = self.sensor_evidence.proximity_near;
|
||||||
|
if raw == believed {
|
||||||
|
// Nothing pending — the sensor agrees with what we already think.
|
||||||
|
self.proximity_since = None;
|
||||||
|
return believed;
|
||||||
|
}
|
||||||
|
let held_for = self
|
||||||
|
.proximity_since
|
||||||
|
.map(|t| now.saturating_duration_since(t))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let threshold = if raw {
|
||||||
|
self.policy.proximity_near_debounce
|
||||||
|
} else {
|
||||||
|
self.policy.proximity_far_debounce
|
||||||
|
};
|
||||||
|
if held_for >= threshold {
|
||||||
|
self.proximity_since = None;
|
||||||
|
raw
|
||||||
|
} else {
|
||||||
|
believed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Let a held reading win once its threshold passes with no new report.
|
||||||
|
///
|
||||||
|
/// Without this the debounce would only resolve when the next reading
|
||||||
|
/// arrives, and a reporter that heartbeats every 30 s would make a real
|
||||||
|
/// `near` take up to half a minute to be believed.
|
||||||
|
fn resolve_proximity_debounce(&mut self, now: Instant) {
|
||||||
|
if self.proximity_raw == self.sensor_evidence.proximity_near {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let held_for = self
|
||||||
|
.proximity_since
|
||||||
|
.map(|t| now.saturating_duration_since(t))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let believed = self.debounce_proximity(self.proximity_raw, now);
|
||||||
|
if believed != self.sensor_evidence.proximity_near {
|
||||||
|
self.sensor_evidence.proximity_near = believed;
|
||||||
|
|
||||||
|
// Record the edge. Without this the trail would carry a
|
||||||
|
// Locked → Observed transition with no sensor input behind it —
|
||||||
|
// the reading was filtered on the way in and believed a tick
|
||||||
|
// later, so nothing would say why the machine moved. Filtered
|
||||||
|
// blips stay out of the trail deliberately; the edge that wins
|
||||||
|
// does not.
|
||||||
|
let confidence = self.sensor_evidence.confidence();
|
||||||
|
let snapshot = self.snapshot("", false, false, false, "", false);
|
||||||
|
self.forensic.append(
|
||||||
|
ForensicEvent::SensorInput {
|
||||||
|
source: SensorSource::Proximity,
|
||||||
|
value: SensorValue::Near(believed),
|
||||||
|
confidence,
|
||||||
|
},
|
||||||
|
snapshot,
|
||||||
|
&format!(
|
||||||
|
"proximity={believed} believed after holding {} ms",
|
||||||
|
held_for.as_millis()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let should_be_observed = self.state == DeviceState::Locked && believed;
|
||||||
|
if should_be_observed {
|
||||||
|
self.transition(DeviceState::Observed);
|
||||||
|
} else if matches!(self.state, DeviceState::Observed) && !believed {
|
||||||
|
self.transition(DeviceState::Locked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Proximity-shaped convenience wrapper.
|
/// Proximity-shaped convenience wrapper.
|
||||||
pub fn update_sensors(&mut self, evidence: SensorEvidence) {
|
pub fn update_sensors(&mut self, evidence: SensorEvidence) {
|
||||||
self.update_sensors_from(SensorSource::Proximity, evidence)
|
self.update_sensors_from(SensorSource::Proximity, evidence)
|
||||||
|
|
@ -1607,6 +1738,27 @@ impl DeviceStateMachine {
|
||||||
/// potentially transition to/from Observed.
|
/// potentially transition to/from Observed.
|
||||||
/// Emits forensic entries for every sensor input.
|
/// Emits forensic entries for every sensor input.
|
||||||
pub fn update_sensors_from(&mut self, source: SensorSource, evidence: SensorEvidence) {
|
pub fn update_sensors_from(&mut self, source: SensorSource, evidence: SensorEvidence) {
|
||||||
|
self.update_sensors_from_at(source, evidence, Instant::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `update_sensors_from` with the clock passed in, so the debounce can be
|
||||||
|
/// tested on a timeline instead of by sleeping.
|
||||||
|
pub fn update_sensors_from_at(
|
||||||
|
&mut self,
|
||||||
|
source: SensorSource,
|
||||||
|
mut evidence: SensorEvidence,
|
||||||
|
now: Instant,
|
||||||
|
) {
|
||||||
|
// Proximity is debounced before it is believed. The rest of the
|
||||||
|
// evidence is taken as reported — none of it flaps the way this one
|
||||||
|
// does, and none of it has the measurement behind it that would justify
|
||||||
|
// picking a threshold.
|
||||||
|
if source == SensorSource::Proximity {
|
||||||
|
evidence.proximity_near = self.debounce_proximity(evidence.proximity_near, now);
|
||||||
|
} else {
|
||||||
|
evidence.proximity_near = self.sensor_evidence.proximity_near;
|
||||||
|
}
|
||||||
|
|
||||||
let was_observed = matches!(self.state, DeviceState::Observed);
|
let was_observed = matches!(self.state, DeviceState::Observed);
|
||||||
let proximity_near = evidence.proximity_near;
|
let proximity_near = evidence.proximity_near;
|
||||||
let should_be_observed = self.state == DeviceState::Locked && proximity_near;
|
let should_be_observed = self.state == DeviceState::Locked && proximity_near;
|
||||||
|
|
@ -2257,6 +2409,151 @@ mod tests {
|
||||||
assert!(!e.should_promote_idle_faster());
|
assert!(!e.should_promote_idle_faster());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Proximity debounce ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A locked machine and a clock, for driving proximity on a timeline.
|
||||||
|
fn locked_for_proximity() -> (DeviceStateMachine, Instant) {
|
||||||
|
let mut sm = DeviceStateMachine::new();
|
||||||
|
sm.transition(DeviceState::Locked);
|
||||||
|
(sm, Instant::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prox(near: bool) -> SensorEvidence {
|
||||||
|
SensorEvidence {
|
||||||
|
proximity_near: near,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report proximity the way `server.rs` does — the reading and the
|
||||||
|
/// freshness stamp together. A test that only does the first half has a
|
||||||
|
/// source that is instantly stale.
|
||||||
|
fn report_prox(sm: &mut DeviceStateMachine, near: bool, at: Instant) {
|
||||||
|
sm.mark_evidence_seen_at(SensorSource::Proximity, at);
|
||||||
|
sm.update_sensors_from_at(SensorSource::Proximity, prox(near), at);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report `near` and let it hold long enough to be believed.
|
||||||
|
fn hold_prox_near(sm: &mut DeviceStateMachine, at: Instant) -> Instant {
|
||||||
|
report_prox(sm, true, at);
|
||||||
|
let settled = at + PROXIMITY_NEAR_DEBOUNCE + Duration::from_millis(1);
|
||||||
|
sm.mark_evidence_seen_at(SensorSource::Proximity, settled);
|
||||||
|
sm.tick_at(settled);
|
||||||
|
settled
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_sub_second_near_blip_is_never_believed() {
|
||||||
|
// The measured case, 2026-07-26: 44 near-episodes in 2.4 hours, median
|
||||||
|
// dwell 1 s, 15 of them sub-second. Each one produced two transitions
|
||||||
|
// and two trail entries for a reading that meant nothing.
|
||||||
|
let (mut sm, t0) = locked_for_proximity();
|
||||||
|
|
||||||
|
report_prox(&mut sm, true, t0);
|
||||||
|
assert_eq!(sm.state, DeviceState::Locked, "near is not believed yet");
|
||||||
|
assert!(!sm.sensor_evidence.proximity_near);
|
||||||
|
|
||||||
|
report_prox(&mut sm, false, t0 + Duration::from_millis(400));
|
||||||
|
sm.tick_at(t0 + Duration::from_millis(500));
|
||||||
|
assert_eq!(
|
||||||
|
sm.state,
|
||||||
|
DeviceState::Locked,
|
||||||
|
"the blip must leave no transition behind"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_held_near_is_believed_once_it_has_held() {
|
||||||
|
// The nine real episodes ran 5 s to 173 s. They must survive untouched.
|
||||||
|
let (mut sm, t0) = locked_for_proximity();
|
||||||
|
report_prox(&mut sm, true, t0);
|
||||||
|
assert_eq!(sm.state, DeviceState::Locked);
|
||||||
|
|
||||||
|
// No further reading arrives — the tick has to finish the job, or a
|
||||||
|
// reporter that heartbeats every 30 s would delay a real near by half
|
||||||
|
// a minute.
|
||||||
|
hold_prox_near(&mut sm, t0);
|
||||||
|
assert!(sm.sensor_evidence.proximity_near);
|
||||||
|
assert_eq!(sm.state, DeviceState::Observed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn far_is_believed_at_once() {
|
||||||
|
// The reading that ends a veto is never the slow one.
|
||||||
|
let (mut sm, t0) = locked_for_proximity();
|
||||||
|
hold_prox_near(&mut sm, t0);
|
||||||
|
assert_eq!(sm.state, DeviceState::Observed);
|
||||||
|
|
||||||
|
let t1 = t0 + Duration::from_secs(10);
|
||||||
|
report_prox(&mut sm, false, t1);
|
||||||
|
assert!(!sm.sensor_evidence.proximity_near);
|
||||||
|
assert_eq!(sm.state, DeviceState::Locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_flapping_sensor_that_never_settles_is_never_believed() {
|
||||||
|
// Alternating faster than the threshold: the near edge keeps restarting
|
||||||
|
// its clock, so nothing is ever believed and the trail stays quiet.
|
||||||
|
let (mut sm, t0) = locked_for_proximity();
|
||||||
|
let mut t = t0;
|
||||||
|
for _ in 0..20 {
|
||||||
|
report_prox(&mut sm, true, t);
|
||||||
|
t += Duration::from_millis(200);
|
||||||
|
report_prox(&mut sm, false, t);
|
||||||
|
t += Duration::from_millis(200);
|
||||||
|
}
|
||||||
|
assert_eq!(sm.state, DeviceState::Locked);
|
||||||
|
assert!(!sm.sensor_evidence.proximity_near);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_heartbeat_repeat_does_not_restart_the_debounce() {
|
||||||
|
// Reporters re-send their last value every 30 s (§10). A repeat is the
|
||||||
|
// same edge continuing, not a new one — if it reset the clock, a held
|
||||||
|
// near would never be believed.
|
||||||
|
let (mut sm, t0) = locked_for_proximity();
|
||||||
|
report_prox(&mut sm, true, t0);
|
||||||
|
report_prox(&mut sm, true, t0 + Duration::from_millis(500));
|
||||||
|
report_prox(
|
||||||
|
&mut sm,
|
||||||
|
true,
|
||||||
|
t0 + PROXIMITY_NEAR_DEBOUNCE + Duration::from_millis(1),
|
||||||
|
);
|
||||||
|
assert!(sm.sensor_evidence.proximity_near);
|
||||||
|
assert_eq!(sm.state, DeviceState::Observed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_non_proximity_reading_does_not_disturb_the_debounce() {
|
||||||
|
// accel/light/touch share the evidence struct; updating one must not
|
||||||
|
// silently overwrite a proximity value mid-debounce.
|
||||||
|
let (mut sm, t0) = locked_for_proximity();
|
||||||
|
report_prox(&mut sm, true, t0);
|
||||||
|
|
||||||
|
let accel = SensorEvidence {
|
||||||
|
proximity_near: false, // the caller's stale copy — must be ignored
|
||||||
|
accel_moving: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
sm.mark_evidence_seen_at(SensorSource::Accelerometer, t0 + Duration::from_millis(100));
|
||||||
|
sm.update_sensors_from_at(
|
||||||
|
SensorSource::Accelerometer,
|
||||||
|
accel,
|
||||||
|
t0 + Duration::from_millis(100),
|
||||||
|
);
|
||||||
|
|
||||||
|
sm.mark_evidence_seen_at(
|
||||||
|
SensorSource::Proximity,
|
||||||
|
t0 + PROXIMITY_NEAR_DEBOUNCE + Duration::from_millis(1),
|
||||||
|
);
|
||||||
|
sm.tick_at(t0 + PROXIMITY_NEAR_DEBOUNCE + Duration::from_millis(1));
|
||||||
|
assert!(sm.sensor_evidence.accel_moving);
|
||||||
|
assert!(
|
||||||
|
sm.sensor_evidence.proximity_near,
|
||||||
|
"the proximity edge must still resolve on its own clock"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_tap_to_wake_is_vetoed_by_a_covered_sensor() {
|
fn only_tap_to_wake_is_vetoed_by_a_covered_sensor() {
|
||||||
// A double tap is the one wake a pocket can produce by itself. The
|
// A double tap is the one wake a pocket can produce by itself. The
|
||||||
|
|
@ -2280,11 +2577,9 @@ mod tests {
|
||||||
sm.transition(DeviceState::Dimmed);
|
sm.transition(DeviceState::Dimmed);
|
||||||
sm.transition(DeviceState::Locked);
|
sm.transition(DeviceState::Locked);
|
||||||
|
|
||||||
let evidence = SensorEvidence {
|
// Held, not blipped — near is debounced now, so a reading that does not
|
||||||
proximity_near: true,
|
// last is a reading the machine never believed.
|
||||||
..Default::default()
|
hold_prox_near(&mut sm, Instant::now());
|
||||||
};
|
|
||||||
sm.update_sensors(evidence);
|
|
||||||
assert_eq!(sm.state(), DeviceState::Observed);
|
assert_eq!(sm.state(), DeviceState::Observed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2295,11 +2590,9 @@ mod tests {
|
||||||
sm.transition(DeviceState::Locked);
|
sm.transition(DeviceState::Locked);
|
||||||
|
|
||||||
// Enter observed
|
// Enter observed
|
||||||
sm.update_sensors(SensorEvidence {
|
let settled = hold_prox_near(&mut sm, Instant::now());
|
||||||
proximity_near: true,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
assert_eq!(sm.state(), DeviceState::Observed);
|
assert_eq!(sm.state(), DeviceState::Observed);
|
||||||
|
let _ = settled;
|
||||||
|
|
||||||
// Leave observed
|
// Leave observed
|
||||||
sm.update_sensors(SensorEvidence {
|
sm.update_sensors(SensorEvidence {
|
||||||
|
|
@ -2354,7 +2647,9 @@ mod tests {
|
||||||
"native IdleMonitor fired, requesting lock",
|
"native IdleMonitor fired, requesting lock",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Phone goes into pocket — proximity near
|
// 4. Phone goes into pocket — proximity near, held. A pocket is not a
|
||||||
|
// one-second blip; the debounce is what tells them apart.
|
||||||
|
hold_prox_near(&mut sm, Instant::now());
|
||||||
sm.update_sensors(SensorEvidence {
|
sm.update_sensors(SensorEvidence {
|
||||||
proximity_near: true,
|
proximity_near: true,
|
||||||
accel_moving: false,
|
accel_moving: false,
|
||||||
|
|
@ -2388,11 +2683,9 @@ mod tests {
|
||||||
sm.transition(DeviceState::Locked);
|
sm.transition(DeviceState::Locked);
|
||||||
sm.record_wake(WakeTrigger::PowerButton, "user pressed power button");
|
sm.record_wake(WakeTrigger::PowerButton, "user pressed power button");
|
||||||
|
|
||||||
// 8. Phone goes back in pocket briefly
|
// 8. Phone goes back in pocket. Held — "briefly" is precisely what the
|
||||||
sm.update_sensors(SensorEvidence {
|
// debounce now refuses to believe, which is the point of it.
|
||||||
proximity_near: true,
|
hold_prox_near(&mut sm, Instant::now());
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
assert_eq!(sm.state(), DeviceState::Observed);
|
assert_eq!(sm.state(), DeviceState::Observed);
|
||||||
|
|
||||||
// 9. Phone comes back out
|
// 9. Phone comes back out
|
||||||
|
|
@ -2526,13 +2819,20 @@ mod tests {
|
||||||
let mut sm = DeviceStateMachine::new();
|
let mut sm = DeviceStateMachine::new();
|
||||||
sm.transition(DeviceState::Dimmed);
|
sm.transition(DeviceState::Dimmed);
|
||||||
sm.transition(DeviceState::Locked);
|
sm.transition(DeviceState::Locked);
|
||||||
// Proximity near + accel moving = walking with phone
|
// Proximity near + accel moving = walking with phone. Near is
|
||||||
sm.update_sensors(SensorEvidence {
|
// debounced, so it has to be held before the pair is on the record.
|
||||||
proximity_near: true,
|
let t0 = Instant::now();
|
||||||
accel_moving: true,
|
hold_prox_near(&mut sm, t0);
|
||||||
light_changing: false,
|
sm.update_sensors_from_at(
|
||||||
touch_active: false,
|
SensorSource::Accelerometer,
|
||||||
});
|
SensorEvidence {
|
||||||
|
proximity_near: true,
|
||||||
|
accel_moving: true,
|
||||||
|
light_changing: false,
|
||||||
|
touch_active: false,
|
||||||
|
},
|
||||||
|
t0 + PROXIMITY_NEAR_DEBOUNCE + Duration::from_millis(2),
|
||||||
|
);
|
||||||
let entries = sm.forensic.recent(10);
|
let entries = sm.forensic.recent(10);
|
||||||
let sensor_entries: Vec<_> = entries
|
let sensor_entries: Vec<_> = entries
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -2540,7 +2840,7 @@ mod tests {
|
||||||
.collect();
|
.collect();
|
||||||
assert!(!sensor_entries.is_empty());
|
assert!(!sensor_entries.is_empty());
|
||||||
// The confidence should reflect the disagreement (0.4 + 0.3 - 0.2 = 0.5)
|
// The confidence should reflect the disagreement (0.4 + 0.3 - 0.2 = 0.5)
|
||||||
match &sensor_entries[0].event {
|
match &sensor_entries[sensor_entries.len() - 1].event {
|
||||||
ForensicEvent::SensorInput { confidence, .. } => {
|
ForensicEvent::SensorInput { confidence, .. } => {
|
||||||
assert!(
|
assert!(
|
||||||
(confidence - 0.5).abs() < 0.01,
|
(confidence - 0.5).abs() < 0.01,
|
||||||
|
|
@ -2708,15 +3008,13 @@ mod tests {
|
||||||
// `near` across a lock flips should_be_observed with no change in the
|
// `near` across a lock flips should_be_observed with no change in the
|
||||||
// evidence at all — an early return here would strand the machine.
|
// evidence at all — an early return here would strand the machine.
|
||||||
let mut sm = DeviceStateMachine::new();
|
let mut sm = DeviceStateMachine::new();
|
||||||
let near = SensorEvidence {
|
let t0 = Instant::now();
|
||||||
proximity_near: true,
|
let settled = hold_prox_near(&mut sm, t0);
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
sm.update_sensors(near.clone());
|
|
||||||
assert_eq!(sm.state, DeviceState::Active);
|
assert_eq!(sm.state, DeviceState::Active);
|
||||||
|
assert!(sm.sensor_evidence.proximity_near, "near is believed by now");
|
||||||
|
|
||||||
sm.transition(DeviceState::Locked);
|
sm.transition(DeviceState::Locked);
|
||||||
sm.update_sensors(near);
|
report_prox(&mut sm, true, settled + Duration::from_secs(1));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sm.state,
|
sm.state,
|
||||||
DeviceState::Observed,
|
DeviceState::Observed,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue