mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-15 08:53:43 +08:00
Sentry: Poll without Message Resulted in Reset of Countdown
This commit is contained in:
@@ -425,6 +425,7 @@ for (const subtitle of ["Off: Motion monitoring is disabled.", "Not armed: Batte
|
||||
assert.equal(sentryMonitorStatus({ subtitle }).detail, subtitle)
|
||||
}
|
||||
assert.equal(sentryMonitorStatus({ subtitle: "Arming · 82s: Monitoring begins after 90 seconds parked." }).tone, "warning")
|
||||
assert.equal(sentryMonitorStatus({ subtitle: "Arming · 82s: Monitoring begins after 90 seconds parked." }).label, "Arming")
|
||||
const armedStatus = { subtitle: "Monitoring motion: Watching for movement while parked." }
|
||||
assert.equal(sentryMonitorStatus(armedStatus).label, "Armed")
|
||||
assert.equal(sentryMonitorStatus(armedStatus, "Connection lost").label, "Status unavailable")
|
||||
|
||||
@@ -741,9 +741,9 @@ html.gx-maker-open, html.gx-maker-open body { touch-action: manipulation; }
|
||||
.gx-model-card--polaris .gx-model-card__title { background: none; -webkit-text-fill-color: currentColor; color: CanvasText; }
|
||||
}
|
||||
|
||||
.gx-sentry-status { flex: 1 1 100%; order: 1; display: grid; gap: var(--sp-2); }
|
||||
.gx-sentry-status__label { width: fit-content; padding: 4px 12px; border: 1px solid currentColor; border-radius: 999px; }
|
||||
.gx-sentry-status--armed { color: var(--success); }
|
||||
.gx-sentry-status--disarmed { color: var(--error); }
|
||||
.gx-sentry-status--warning { color: var(--warning); }
|
||||
.gx-sentry-status { flex: 1 1 100%; order: 1; display: grid; gap: var(--sp-1); color: var(--on-surface-variant); font-size: var(--fs-sm); }
|
||||
.gx-sentry-status__label { display: inline-flex; align-items: center; gap: var(--sp-2); width: fit-content; font-weight: 500; }
|
||||
.gx-sentry-status__label::before { content: ""; width: 6px; height: 6px; flex: 0 0 6px; border-radius: 50%; background: var(--sentry-status-dot, currentColor); }
|
||||
.gx-sentry-status--armed { --sentry-status-dot: var(--success); }
|
||||
.gx-sentry-status--warning { --sentry-status-dot: var(--warning); }
|
||||
.gx-sentry-status__detail { color: var(--on-surface-variant); font-size: var(--fs-sm); }
|
||||
|
||||
@@ -49,7 +49,8 @@ export function sentryMonitorStatus(data, error = "") {
|
||||
if (error || !subtitle || label.startsWith("Waiting for monitor") || label === "Unavailable" || subtitle.includes("status is unavailable"))
|
||||
return { label: "Status unavailable", tone: "warning", detail: error ? "Reconnecting to the motion monitor…" : subtitle || "Checking motion monitor…" }
|
||||
if (label === "Monitoring motion") return { label: "Armed", tone: "armed", detail: subtitle }
|
||||
return { label: "Disarmed", tone: label.startsWith("Arming") ? "warning" : "disarmed", detail: subtitle }
|
||||
if (label.startsWith("Arming")) return { label: "Arming", tone: "warning", detail: subtitle }
|
||||
return { label: "Disarmed", tone: "disarmed", detail: subtitle }
|
||||
}
|
||||
|
||||
export class SettingsFeed {
|
||||
|
||||
@@ -31,6 +31,8 @@ threshold and warning-count behavior. An alarm requires more than 25 qualifying
|
||||
deltas and at least 30 seconds within a 60-second motion window. A stale or
|
||||
repeated sample, sensor loss, owner-loop gap, system resume, settings change,
|
||||
ignition or shutdown request disarms the policy. Recovery requires rearming.
|
||||
Polling without a new sensor message preserves arming while the last accepted
|
||||
sample remains fresh (at most 0.5 seconds); it never counts as another motion sample.
|
||||
Hardwared retains battery accounting and shutdown authority; Sentry neither
|
||||
extends parked runtime nor clears shutdown requests.
|
||||
|
||||
|
||||
@@ -54,10 +54,11 @@ class Inputs:
|
||||
device_fresh: bool
|
||||
ignition_off: bool
|
||||
sample: MotionSample | None = None
|
||||
sample_pending: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if any(type(value) is not bool for value in (self.enabled, self.offroad, self.voltage_ok,
|
||||
self.device_fresh, self.ignition_off)) or \
|
||||
self.device_fresh, self.ignition_off, self.sample_pending)) or \
|
||||
(self.sample is not None and not isinstance(self.sample, MotionSample)):
|
||||
raise ValueError("Sentry authority inputs must be explicit booleans and a validated motion sample")
|
||||
|
||||
@@ -123,6 +124,12 @@ class SentryPolicy:
|
||||
return Decision("unavailable")
|
||||
|
||||
sample = inputs.sample
|
||||
# Polling without a new message is not sensor loss. Preserve continuity only
|
||||
# while the last accepted sample is fresh, without counting it as motion.
|
||||
if sample is None and inputs.sample_pending and self.arm_started is not None and \
|
||||
self.last_sample_time is not None and 0 <= now - self.last_sample_time <= MAX_SAMPLE_AGE_SECONDS:
|
||||
remaining = ARM_DELAY_SECONDS - (now - self.arm_started)
|
||||
return Decision("arming", seconds_remaining=max(0, math.ceil(remaining))) if remaining > 0 else Decision("armed")
|
||||
if sample is None or sample.mono_time > now or now - sample.mono_time > MAX_SAMPLE_AGE_SECONDS or \
|
||||
(self.last_sample_time is not None and sample.mono_time <= self.last_sample_time):
|
||||
last_sample_time = self.last_sample_time
|
||||
|
||||
@@ -157,7 +157,10 @@ class SentryRuntime:
|
||||
self.saved_raw = saved.raw
|
||||
offroad, ignition_off, voltage_ok, fresh, now_ns = self._authority()
|
||||
sample = self._sample(now_ns) if active else None
|
||||
decision = self.policy.update(now_ns / 1e9, Inputs(active, offroad, voltage_ok, fresh, ignition_off, sample))
|
||||
sm = self.messages
|
||||
sample_pending = bool(not sm.updated["accelerometer"] and sm.seen["accelerometer"] and
|
||||
sm.alive["accelerometer"] and sm.valid["accelerometer"])
|
||||
decision = self.policy.update(now_ns / 1e9, Inputs(active, offroad, voltage_ok, fresh, ignition_off, sample, sample_pending))
|
||||
expected_raw = saved.raw
|
||||
if decision.event is not None and self.record is not None and expected_raw is not None and self._permitted(expected_raw):
|
||||
try:
|
||||
|
||||
@@ -74,6 +74,22 @@ def test_missing_sensor_during_arming_and_after_armed_requires_full_rearm():
|
||||
assert policy.update(180.5, active(motion(180.5, 9.8))).state == "arming"
|
||||
|
||||
|
||||
def test_pending_messages_preserve_countdown_but_never_count_as_motion():
|
||||
policy = SentryPolicy()
|
||||
pending = Inputs(True, True, True, True, True, sample_pending=True)
|
||||
assert policy.update(0.0, pending).state == "sensor_unavailable"
|
||||
for tenth in range(1, 905):
|
||||
now = tenth / 10
|
||||
decision = policy.update(now, active(motion(now, 9.8)) if tenth % 2 else pending)
|
||||
assert decision.state == ("arming" if now < 90.1 else "armed")
|
||||
assert decision.event is None
|
||||
assert policy.trigger_count == 0
|
||||
# The previous sample is 90.3; repeated polls cannot renew its freshness.
|
||||
assert policy.update(90.7, pending).state == "armed"
|
||||
assert policy.update(90.9, pending).state == "sensor_unavailable"
|
||||
assert policy.update(91.0, active(motion(91.0, 9.8))).seconds_remaining == 90
|
||||
|
||||
|
||||
def test_disabled_onroad_low_voltage_and_lost_device_rearm_for_full_90_seconds():
|
||||
for stopped, expected in ((Inputs(False, True, True, True, True), "disabled"),
|
||||
(Inputs(True, False, True, True, True), "disabled_onroad"),
|
||||
|
||||
@@ -90,6 +90,7 @@ class TestRuntime(unittest.TestCase):
|
||||
status.publish.assert_called_once_with(decision, encode(Preferences(True)), "idle")
|
||||
self.assertEqual(decision.state, "arming")
|
||||
self.messages.updated["accelerometer"] = False
|
||||
self.messages.valid["accelerometer"] = False
|
||||
self.clock.mono += 100_000_000
|
||||
decision = self.runtime.tick()
|
||||
self.assertEqual(decision.state, "sensor_unavailable")
|
||||
@@ -101,10 +102,22 @@ class TestRuntime(unittest.TestCase):
|
||||
self.assertIsNotNone(self.runtime.policy.last_sample_time)
|
||||
self.messages.updated["accelerometer"] = False
|
||||
self.clock.mono += 100_000_000
|
||||
self.assertEqual(self.runtime.tick().state, "arming")
|
||||
self.clock.mono += 400_000_000
|
||||
self.assertEqual(self.runtime.tick().state, "sensor_unavailable")
|
||||
self.assertIsNone(self.runtime.policy.arm_started)
|
||||
self.assertEqual(self.events, [])
|
||||
|
||||
def test_arming_completes_with_brief_gaps_between_sensor_messages(self):
|
||||
for _ in range(451):
|
||||
self.tick()
|
||||
self.messages.updated["accelerometer"] = False
|
||||
self.clock.mono += 100_000_000
|
||||
decision = self.runtime.tick()
|
||||
self.assertIn(decision.state, ("arming", "armed"))
|
||||
self.assertEqual(decision.state, "armed")
|
||||
self.assertEqual(self.events, [])
|
||||
|
||||
|
||||
def test_serialized_device_panda_peripheral_use_actual_clock_domains(self):
|
||||
self.clock.mono += 100_000_000
|
||||
|
||||
Reference in New Issue
Block a user