DEVICE-STATE-MACHINE.md said "partly built and entirely inert" and the archived
deploy task said "observational only". Both were written 2026-07-25 midday and
were overtaken by aa30278 the same afternoon. tasks/README.md already said
"actuating 07-25" — the index was right and the design docs were two hours
behind, which read as a live blocker on TASK-25's ordering note ("state-machine
controls must wait for TASK-15/08 to give it actuators").
Doctrine: where docs and code disagree, the code is right.
Also records the second pass: lock-before-blank is now an invariant enforced in
the authority, and hypridle's competing listeners are deleted.
27 KiB
Device state machine — unified design
Status: actuating on hardware since 2026-07-25. Created: 2026-07-24.
b4b30b1 landed the shape: the §2 enum (all 8 variants), a 20-pair
LEGAL_TRANSITIONS guard, §4's confidence arithmetic with exactly the weights
below, and the forensic log. It was entirely inert for a day — no caller
for transition() outside update_sensors, no sensor feeder, no clock, so
the daemon never left Active and the forensic log stayed at count: 0.
aa30278 gave it the missing half and it now drives the device:
- a 1 s tick loop (
tick_at()takes the clock, so rules test without sleeping) - real actuators —
Dim/Restore/Blankthrough brightnessctl,blueline-undim, and the DPMS executor - input truth from
ext-idle-notify(idle.rs), not inferred, which is what keeps a long swipe from being blanked mid-gesture panelas a field, not a ninth state: the enum had no cell for "locked, screen dark", and DozeLight is not a dark glance
Verified on the phone: panel-dim then panel-off in forensic.jsonl,
including an error-operational entry when brightnessctl exited 1 — the
§5 contract working end to end.
Ordering is now an invariant, not a coincidence (2026-07-25, second pass).
Every path to a dark panel routes through one function that locks first, waits
LOCK_ACK_BUDGET (2 s) for the compositor ack, and blanks anyway on timeout
while recording an error-security — the panel fails open, the claim never
does (LOCK-DPMS-LESSONS §1 + doctrine §8). hypridle's 300 s lock and 600 s
screen-off listeners are deleted, not disabled: they were the competing
writer that made §1 hold only because 300 < 600.
Still open: the sensor feeder (sensor_input has senders now via
blueline-proximity-lock, but SLPI health gating is not built), doze tiers
(TASK-15, blocked on slice separation), and both confidence gates are still
computed and logged without being branched on.
Two divergences to settle when wiring, code-first per doc precedence:
- The
Locked → Observedtransition keys on rawproximity_near, not on the §4 threshold table. - §4 says proximity-near + accel-moving is hand-held so DPMS wake is NOT suppressed; the code computes 0.4+0.3−0.2 = 0.5 ≥ 0.3 and suppresses it anyway (its own test asserts the suppression).
Not in this doc, and needed before any of it can be trusted: evidence-source
health is device state. On 2026-07-25 SLPI took a CHRE fatal, remoteproc
recovered it, blueline-hexagonrpcd-sdsp exited "successfully" and was never
restarted — every sensor was dead for hours and nothing noticed, because a
consumer of a dead sensor looks identical to one whose sensor is quiet. See
Pixel3Arch/PAF/slpi.md. "No evidence" and "evidence says nothing is
happening" must not be the same state.
Supersedes: the scattered state machines in IdleCoordinator.qml (7 states),
sessiond server.rs Phase (4 states), blueline-screen-toggle (2 states), and
blueline-proximity-lock (2 states). Those are projections of this model, not
separate systems.
1. Why one machine, not seven
The phone is in one state at any moment. It is either in your hand, in your pocket, on a table, or asleep. Each of the seven actors we built sees one facet and acts on it blindly:
- IdleCoordinator sees idle timers → locks at 300s
- sessiond sees heartbeat → retakes lock on EOF
- blueline-screen-toggle sees DPMS commands → serializes panel power
- blueline-proximity-lock sees proximity sensor → blanks locked screen
- SessionEvents sees PrepareForSleep → lock-then-release inhibitor
- hypridle sees idle timeout → dispatches lock/DPMS
- GlobalStates is a flat bag of booleans with no transition logic
None of them can see the others. Three of them can turn the screen off. Two of them can request a lock. None of them knows whether the phone is in a pocket or on a table — they just know "the user stopped touching it."
The doctrine already says this (SESSION-AUTHORITY-DOCTRINE §9): "Sensor readings
are evidence, not fact." The tier vocabulary needs a row the current matrix has
no cell for — observed. This doc defines that row.
2. The unified state
One enum. The device is in exactly one state at any time.
enum DeviceState {
Active, // Screen on, user present, unlocked or lockable
Dimmed, // Screen dim, user idle, not yet locked
Locked, // Screen locked, compositor secure, user may/may not be present
Observed, // Sensor says user present (proximity, accel, light) —
// EVIDENCE, not FACT. The shell acts on it but never
// trusts it for lock/unlock decisions.
DozeLight, // Locked + idle N min. App tier frozen. Wi-Fi power-save.
DozeDeep, // Locked + idle M min. Network fetchers stopped. RTC wake only.
Suspending, // PrepareForSleep(true). Inhibitor held. Waiting for lock secure.
Asleep, // s2idle. RTC + modem IRQs only.
}
What each state means
Active — the user is interacting with the device. Screen is on. The session
may be unlocked (personal tier) or locked (the user is looking at the lock
surface). displayActive = true. All pollers run at full rate.
Dimmed — the user stopped touching the device but hasn't been idle long
enough to lock. Screen brightness lowered. The native IdleMonitor fired at
dimAfterSeconds. displayActive = false — pollers that produce stale data
should back off.
Locked — the compositor has acknowledged the lock surface (screenLockSecure).
The session is secure. The user may or may not be physically present — the lock
persists across pocketing, table-placing, and suspend/resume. Only PAM auth
unlocks.
Observed — one or more sensors report user presence, but this is EVIDENCE, not FACT. The device is locked (Observed is a sub-state of Locked), and the sensors are saying "someone is here." This state gates:
- DPMS wake (proximity-near suppresses wake to prevent pocket-dial)
- Touch input suppression (face-down on table shouldn't accept accidental touches)
- Idle tier promotion (proximity-near + locked = promote to Doze faster)
Observed NEVER gates:
- Lock/unlock decisions (only PAM does that)
- Security tier transitions (only sessiond does that)
- Personal data disclosure (only screenLockSecure does that)
DozeLight — locked and idle for N minutes. The app tier is frozen
(systemctl --user freeze app-*.scope). Wi-Fi drops to power-save. UPower
polling coasts. The lockscreen glance data (media, agent, notify cards) still
updates on wake — cards read on unlock, they don't need background freshness.
DozeDeep — locked and idle for M minutes. Network fetchers stopped. Only RTC wakeups and modem IRQs can bring the device out. The coordinator wakes every K minutes to check for notifications, then re-suspends.
Suspending — logind's PrepareForSleep(true) arrived. The delay inhibitor
is held. The lock is being requested (if not already secure). This state is
transient — the device moves to Asleep once the inhibitor is released.
Asleep — s2idle. The panel is off. The touch controller is in gesture mode (dt2w). Only hardware wake sources work: power button, RTC alarm, modem IRQ.
3. Transitions
Every transition is guarded. Illegal transitions are refused and logged.
┌─────────┐ idle timeout ┌────────┐
│ Active │ ───────────────→ │ Dimmed │
└────┬─────┘ └───┬────┘
│ │
│ idle lock timer │ idle lock timer
│ external lock signal │ external lock signal
│ │
▼ ▼
┌─────────────────────────────────────┐
│ Locked │
│ (compositor secure, PAM required │
│ to leave) │
└────┬──────────┬──────────┬──────────┘
│ │ │
│ proximity │ idle N │ PrepareForSleep(true)
│ near │ min │
▼ ▼ │
┌──────────┐ ┌───────────┐ │
│ Observed │ │ DozeLight │ │
│ (sensor │ │ (frozen, │ │
│ evidence)│ │ Wi-Fi │ │
└────┬─────┘ │ save) │ │
│ └─────┬─────┘ │
│ │ │
│ proximity │ idle M │
│ far │ min │
│ ▼ │
│ ┌───────────┐ │
│ │ DozeDeep │ │
│ │ (network │ │
│ │ off, RTC │ │
│ │ wake) │ │
│ └─────┬─────┘ │
│ │ │
└─────────────┘ │
(prox far → back │
to Locked) ▼
┌───────────┐
│Suspending │
│(inhibitor │
│ held) │
└─────┬─────┘
│ lock secure
│ inhibitor released
▼
┌───────────┐
│ Asleep │
│(s2idle) │
└─────┬─────┘
│ PrepareForSleep(false)
│ power button / dt2w / RTC
▼
┌───────────┐
│ Locked │
│(wake, lock│
│ persists) │
└───────────┘
Transition table
| From | To | Trigger | Guard | Action |
|---|---|---|---|---|
| Active | Dimmed | native IdleMonitor (dimAfterSeconds) | no inhibitor, no lock requested | lower brightness |
| Active | Locked | native IdleMonitor (lockAfterSeconds) | no inhibitor | request lock |
| Active | Locked | external lock signal (logind Lock) | — | request lock |
| Dimmed | Active | user input (touch, key) | not locked | restore brightness |
| Dimmed | Locked | native IdleMonitor (lockAfterSeconds) | no inhibitor | request lock |
| Locked | Active | PAM auth success | stepUp tier | unlock session |
| Locked | Observed | proximity-near detected | screen locked | suppress DPMS wake |
| Locked | DozeLight | idle N min while locked | screen locked | freeze app tier, Wi-Fi save |
| Observed | Locked | proximity-far detected | screen locked | allow DPMS wake |
| Observed | DozeLight | idle N min while locked | screen locked | freeze app tier |
| DozeLight | Locked | user input (dt2w, power button) | — | thaw app tier |
| DozeLight | DozeDeep | idle M min while locked | screen locked | stop network fetchers |
| DozeDeep | Locked | RTC wakeup, modem IRQ, user input | — | resume network |
| Any pre-sleep | Suspending | PrepareForSleep(true) | — | request lock, hold inhibitor |
| Suspending | Asleep | lock secure + inhibitor released | screenLockSecure | logind proceeds |
| Asleep | Locked | PrepareForSleep(false) | — | reacquire inhibitor |
The Observed transition in detail
Observed is not a destination — it's a modifier on Locked. When the device is Locked and proximity goes near, the device enters "Locked + Observed" — the lock persists, but DPMS wake is suppressed and the idle tier promotion path activates.
When proximity goes far, the device returns to "Locked" — DPMS wake is re-enabled, and the idle timer resumes from where it left off.
This means Observed is really a boolean flag on the Locked state, not a separate state in the enum. The enum above lists it separately for clarity, but the implementation should model it as:
struct DeviceState {
state: DeviceStateEnum, // Active, Dimmed, Locked, DozeLight, DozeDeep, Suspending, Asleep
observed: bool, // sensor evidence of user presence
observed_confidence: f32, // 0.0–1.0, cross-sensor verified
observed_sources: Vec<SensorSource>, // which sensors contributed
}
4. Sensor evidence model
The doctrine §9 says: "Sensor readings are evidence, not fact." This section defines how sensor inputs feed the state machine without becoming authorities.
Sensor sources
| Sensor | What it reports | Confidence | Can it lie? |
|---|---|---|---|
| Proximity (iio-sensor-proxy) | near/far | binary, high | yes — a finger over the sensor |
| Accelerometer (iio-sensor-proxy) | device orientation + motion | continuous, medium | yes — vibration, vehicle |
| Light sensor (iio-sensor-proxy) | ambient light level | continuous, medium | yes — flashlight, shadow |
| Touch (FTS controller) | user input | binary, high | yes — pocket touch |
| Power button | user intent | binary, absolute | no — hardware signal |
| dt2w (FTS gesture mode) | user wake intent | binary, high | yes — false positive |
Evidence aggregation
A single sensor saying "user present" is weak evidence. Two sensors agreeing
is stronger. The observed_confidence field aggregates:
confidence = 0.0
if proximity.near: confidence += 0.4
if accel.moving: confidence += 0.3
if light.changing: confidence += 0.2
if touch.active: confidence += 0.1
// Cap at 1.0
The thresholds for action:
| Confidence | Action allowed |
|---|---|
| < 0.3 | No sensor-driven action. Device stays in current state. |
| 0.3–0.6 | Suppress DPMS wake (prevent pocket-dial). Do not promote idle tier. |
| 0.6–0.8 | Suppress DPMS wake. Promote to DozeLight faster (halve the idle timer). |
| > 0.8 | Suppress DPMS wake. Promote to DozeLight/DozeDeep at normal rate. |
Cross-sensor verification
If sensors disagree, the system logs the disagreement and uses the lower confidence:
- Accelerometer says face-down, light sensor says bright → one is lying.
Log:
[sensor] cross-sensor disagreement: accel=face-down light=bright. Confidence drops to 0.3 (no action). - Proximity says near, accelerometer says moving → user is walking with phone
in hand. Not a pocket scenario. Log:
[sensor] proximity-near + accel-moving: hand-held. Confidence stays high but DPMS wake is NOT suppressed (the user is actively using the phone, just not touching the screen).
What sensors NEVER do
- Unlock the session (only PAM does that)
- Change screenLocked or screenLockSecure (only sessiond does that)
- Serve as a factor in authentication
- Override a manual lock (user pressed power button to lock)
5. Error and audit contract
One trail, not two
Today, SessionAudit tracks state transitions (lock, unlock, sleep, wake) and QML console.log tracks everything else. They never meet. This means:
- A brightnessctl failure is invisible to the audit trail
- A PAM timeout is logged to console but not to the tamper-evident log
- A D-Bus parse error in SessionEvents is a console.log line that vanishes
The unified model says: every event that affects device state goes to one trail. State transitions, sensor inputs that triggered transitions, errors that prevented transitions, and errors that degraded guarantees.
Event categories
| Category | Example | Severity | Trail? |
|---|---|---|---|
| State transition | Active → Dimmed | info | yes |
| Sensor input | proximity-near, confidence=0.7 | debug | yes (if triggered transition) |
| Error — operational | brightnessctl restore failed | warn | yes |
| Error — security | PAM timeout, lock not secure before suspend | error | yes |
| Error — system | D-Bus parse error, gdbus monitor exited | error | yes |
| Error — degraded | sleep inhibitor exited unexpectedly | warn | yes |
| Audit — hash chain | chain resumed, chain reset | info | yes |
Error severity definitions
debug — information useful for development. Not shown to user. Logged to trail only if it triggered a state transition.
info — normal operation. State transitions, successful auth, successful inhibitor acquire/release. Always logged to trail.
warn — degraded operation. The system is working but a guarantee is weaker than intended. Examples: brightnessctl restore failed (screen stays dim), sleep inhibitor exited unexpectedly (suspend will proceed without delay). Logged to trail AND to console.warn.
error — broken operation. A guarantee is violated. Examples: PAM timeout (lock not secure before suspend deadline), D-Bus monitor exited (PrepareForSleep signals will not be received). Logged to trail AND to console.error AND surfaced to the user if security-relevant.
The trail format
The existing SessionAudit hash-chained JSONL format is correct. Extend it with new event types:
{"seq":42,"prev":"sha256...","ts":1234567890,"event":"state-transition","data":{"from":"active","to":"dimmed","trigger":"idle-monitor","confidence":null}}
{"seq":43,"prev":"sha256...","ts":1234567891,"event":"sensor-input","data":{"source":"proximity","value":"near","confidence":0.7,"triggered":true}}
{"seq":44,"prev":"sha256...","ts":1234567892,"event":"error-operational","data":{"component":"idle-coordinator","action":"brightness-restore","error":"exit code 1"}}
{"seq":45,"prev":"sha256...","ts":1234567893,"event":"error-security","data":{"component":"session-events","action":"lock-for-suspend","error":"WlSessionLock.secure not received before deadline"}}
6. Mapping to existing code
What stays
| Component | Role in unified model |
|---|---|
| IdleCoordinator.qml | Projection: owns the QML-side state enum, publishes displayActive, drives brightness. Becomes a consumer of the unified state, not the source. |
| sessiond server.rs Phase | Projection: owns the lock handoff. Phase maps to DeviceState.Locked (Holding/Released are sub-states of Locked). |
| blueline-screen-toggle | Executor: serializes DPMS transitions. Receives commands from the unified state machine, not from independent actors. |
| blueline-proximity-lock | Sensor input: feeds proximity evidence into the unified state machine. Stops making DPMS decisions directly. |
| SessionEvents.qml | Ingress: feeds PrepareForSleep and external lock signals into the unified state machine. |
| SessionAudit.qml | Trail: receives all events from the unified state machine. |
| GlobalStates.qml | Projection: flat booleans consumed by QML surfaces. Driven by the unified state machine. |
What changes
-
The unified state machine is the authority. It lives in Rust (sessiond or a sibling daemon) because it needs to be alive before the shell starts and survive shell crashes. QML projects it; Rust owns it.
-
Sensors become inputs, not actors. blueline-proximity-lock stops calling blueline-screen-toggle directly. It reports proximity-near to the unified state machine, which decides whether to suppress DPMS wake based on confidence and current state.
-
DPMS becomes an executor, not an authority. blueline-screen-toggle keeps its flock mutex and state file, but it receives commands from the unified state machine instead of from three independent callers.
-
The audit trail is unified. SessionAudit receives all events — transitions, sensor inputs, errors. The hash chain covers everything.
-
Error handling is standardized. QML uses console.error for errors, console.warn for warnings, console.log for info. Each log line has a canonical tag. Errors that affect device state go to the audit trail.
7. Comparison targets
This design should be compared to:
- AOSP PowerManagerService — the Android power state machine (Interactive → Dream → Asleep). How does it handle sensor fusion? Does it have an "observed" state? How does it handle drop detection?
- LineageOS doze — any modifications to AOSP's doze implementation
- SailfishOS mce — the mode control entity (libwakelock.c + datapipe.c pub/sub). Cited in TASK-08 as prior art.
- FuriLabs batman — dual-source idle: logind IdleHint + wlr-output-management
- DankMaterialShell — independent idle monitors for dim/DPMS/lock/suspend, AC-vs-battery thresholds
Research complete — findings in §9 below.
8. Open questions
-
Where does the unified state machine live? Answer: sessiond. Doctrine §11 names it as the natural home. It already owns lock state, survives shell crashes, and the boot-order doc (Phase C) says "sessiond is then the natural home for the rest of the authority surface." TASK-15 agrees: "likely a sessiond sibling or sessiond itself."
-
How does the shell read the unified state? Answer: extend
session.state()IPC. The current response{locked, shell_alive, phase}expands to include the device state enum, observed confidence, and doze tier. GlobalStates stays as a QML projection for backward compatibility. -
What is the freeze granularity? Answer: freeze
apps.slice, allowlistapps-wake.slice. TASK-15: "A slice freeze is one command and no bookkeeping; per-app lets Chatty stay live for SMS-adjacent work. Likely: freezeapps.slice, keep an allowlist slice for the few that may run locked." -
How does drop detection fit? Dropped. No existing OS does it. Novel but not needed for v1.
-
What is the RTC wakeup schedule? Answer: M is a config value. TASK-15: "wake every M min → thaw the network fetchers for a bounded window." The coordinator needs a "fetch complete" signal, not a fixed sleep. The wake window is bounded by the fetch, not by a timer. Charging can skip tiers 2–3 (desk-clock case).
9. Prior art — what others do (research findings, 2026-07-24)
AOSP PowerManagerService
Android's top-level power state machine has four states:
WAKEFULNESS_ASLEEP (0) ↔ WAKEFULNESS_AWAKE (1) ↔ WAKEFULNESS_DREAMING (2) ↔ WAKEFULNESS_DOZING (3)
Managed per PowerGroup (one per display group). Transitions are dirty-flag
driven — updatePowerStateLocked() is called whenever wakefulness, user
activity, display power, wake locks, proximity, battery, or settings change.
Guards are eventTime ordering (wakeUpLocked requires eventTime >= lastSleepTime).
The Sandman (dream manager) asynchronously starts/stops dreams and doze.
canDreamLocked() checks battery, charging, dream settings. canDozeLocked()
checks doze enabled, screen-off timeout, ambient suppression.
DozeMachine (SystemUI)
A separate 12-state machine inside the DOZING umbrella, managing the ambient display (AOD) experience:
UNINITIALIZED → INITIALIZED → {DOZE, DOZE_AOD, DOZE_AOD_DOCKED}
DOZE ↔ DOZE_AOD ↔ DOZE_AOD_PAUSING ↔ DOZE_AOD_PAUSED
{DOZE, DOZE_AOD} → DOZE_REQUEST_PULSE → DOZE_PULSING → DOZE_PULSE_DONE → {DOZE, DOZE_AOD}
Proximity is the key guard. Before any pulse, proximityCheckThenCall() checks
if the device is in-pocket — if proximity reports NEAR, the pulse is dropped.
In DOZE_AOD, proximity NEAR transitions to PAUSING then PAUSED (AOD off).
Proximity FAR transitions back to DOZE_AOD.
DozeTriggers — sensor bridge
Registers 9 sensor types for doze interactions:
| Sensor | Trigger | Gated by |
|---|---|---|
| Significant Motion | pulse on movement | proximity check |
| Pick-up Gesture | gentle wake on pickup | proximity check |
| Double Tap | pulse on double-tap | proximity check |
| Tap | pulse on single-tap | proximity check |
| Long Press | pulse on long-press | proximity check |
| UDFPS Long Press | fingerprint long-press | proximity check |
| Wake Display | presence detection to wake AOD | proximity check |
| Wake Lock Screen | reach detection to wake lock | proximity check |
| Quick Pickup | fast pickup for UDFPS devices | proximity check |
All gated by proximity. Sensors are selectively registered based on doze state.
AttentionDetector — camera-based "Smart Stay"
Uses the front-facing camera to detect if the user is looking at the screen. Extends the screen timeout (starts 2 seconds before dim). Has a maximum extension limit to prevent security issues (device never locking). Tracks consecutive timeout extensions.
This is the closest thing to our "observed" state — but it only extends screen timeout, never affects lock/unlock. No confidence scoring, no cross-sensor verification, no fusion with proximity or accelerometer.
DisplayPowerProximityStateController
Low-level proximity-display controller:
- States: PROXIMITY_UNKNOWN (-1), PROXIMITY_NEGATIVE (0), PROXIMITY_POSITIVE (1)
- Positive debounce: 0ms (immediate)
- Negative debounce: 250ms
- Threshold: 5.0cm
- When positive, display can be blanked to prevent accidental touches
Drop detection — does NOT exist in AOSP or LineageOS
No stock Android or LineageOS has drop detection as a state machine input.
Vendor-specific implementations found:
| Vendor | Implementation | Purpose |
|---|---|---|
| Vivo (OPPO/OnePlus parent) | TYPE_DROP_DET sensor (type ID 66548) in PopupCameraManagerService |
Retract popup camera on drop. Hardware protection only. Not a power state input. |
| HarmonyOS (Huawei) | SENSOR_TYPE_DROP_DETECTION = 7 in CategoryMotion |
First-class sensor type in their API. Purpose: hardware protection. |
| Apple Watch | CMFallDetectionEventUserResolution in CoreMotion |
Fall detection for health/safety. Not phone power management. |
Open-source research implementation (sense-android-library FallDetector):
- Phase 1 (free fall): acceleration < 0.6g for > 60ms
- Phase 2 (impact): acceleration > 2.0g within 200ms after free fall
- Phase 3 (inactivity): acceleration < 1.3g for > 2000ms within 3500ms after impact
This is a health app algorithm, not an OS integration. No mobile OS treats drop detection as a power state machine input.
What this means for our design
-
Our "observed" state is novel. No existing OS combines proximity + accelerometer + light into a confidence score for device state decisions. Android uses proximity as a binary gate (suppress/allow), never as evidence.
-
Our sensor fusion model is novel. Android's DozeTriggers gates every sensor through proximity, but never cross-validates sensors against each other. Our "accelerometer says face-down, light says bright — one is lying" pattern doesn't exist anywhere.
-
Drop detection as a state machine input is novel. The three-phase free-fall/impact/inactivity algorithm exists in research, but no OS integrates it into power management. Our design can add it as a sensor source with high confidence (free-fall is hard to fake) that triggers:
- Suppress touch input (prevent accidental taps during fall)
- Lock immediately (if screen was on)
- Log to audit trail (drop event with timestamp + sensor data)
-
The two-machine pattern (PowerManager + DozeMachine) validates our approach. Android separates the top-level wakefulness from the doze UI layer. Our design separates the unified device state from the shell projections (IdleCoordinator, GlobalStates). Same shape, better coverage.
-
Proximity debounce matters. Android's DisplayPowerProximityStateController uses 0ms positive debounce (immediate) and 250ms negative debounce (wait before declaring "far"). Our design should adopt this — proximity-near should suppress DPMS immediately, but proximity-far should wait 250ms before re-enabling it (prevents flicker when the sensor is noisy).