This commit is contained in:
firestar5683
2026-08-16 21:09:58 -05:00
parent 33744deb61
commit b567d9fc90
8 changed files with 138 additions and 10 deletions
+5 -2
View File
@@ -4,13 +4,16 @@ from pathlib import Path
MIN_DATE = datetime.datetime(year=2025, month=2, day=21)
MAX_DATE = datetime.datetime(year=2035, month=1, day=1)
def _utc_now_naive():
return datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
def min_date():
# on systemd systems, the default time is the systemd build time
systemd_path = Path("/lib/systemd/systemd")
if systemd_path.exists():
d = datetime.datetime.fromtimestamp(systemd_path.stat().st_mtime)
d = datetime.datetime.fromtimestamp(systemd_path.stat().st_mtime, datetime.UTC).replace(tzinfo=None)
return max(MIN_DATE, d + datetime.timedelta(days=1))
return MIN_DATE
def system_time_valid():
return min_date() < datetime.datetime.now() < MAX_DATE
return min_date() < _utc_now_naive() < MAX_DATE
+2 -1
View File
@@ -43,7 +43,8 @@ if __name__ == '__main__':
with tempfile.TemporaryDirectory() as tmp:
for directory in DIRS:
shutil.copytree(BASEDIR + '/' + directory, tmp + '/' + directory, symlinks=False, dirs_exist_ok=True, copy_function=copy)
shutil.copytree(BASEDIR + '/' + directory, tmp + '/' + directory, symlinks=False, ignore_dangling_symlinks=True,
dirs_exist_ok=True, copy_function=copy)
entry = f'{args.module}:{args.entrypoint}'
zipapp.create_archive(tmp, target=args.output, interpreter=INTERPRETER, main=entry)
@@ -584,6 +584,23 @@ def test_route_listing_uses_all_segment_times_when_segment_zero_was_touched(tmp_
assert end == "2026-07-18T07:22:00"
def test_route_listing_prefers_logged_time_after_offline_clock_reset(tmp_path, monkeypatch):
route_start = utilities.datetime(2026, 7, 18, 7, 19, 0)
route_name = "000011e3--6e01289631"
segment = tmp_path / f"{route_name}--0"
segment.mkdir()
(segment / "qlog.zst").write_bytes(b"placeholder")
stale_time = utilities.datetime(2025, 7, 18, 7, 20, 0).timestamp()
os.utime(segment, (stale_time, stale_time))
monkeypatch.setattr(utilities, "_route_logged_start_time", lambda _path: route_start)
routes = utilities._list_dashboard_routes([tmp_path])
assert routes[0]["startedAt"] == route_start
assert routes[0]["timeSource"] == utilities.DASHBOARD_TIME_SOURCE_LOG
def test_top_models_are_ranked_from_persisted_usage_not_favorites():
params = FakeParams({
"AvailableModels": "orion,vega,atlas,nova",
+52 -4
View File
@@ -994,9 +994,18 @@ def _select_dashboard_segment_candidate(candidates):
return next((candidate for candidate in candidates if _segment_has_dashboard_log(candidate)), candidates[0])
def _estimate_route_started_at(segments):
def _estimate_route_start_details(segments):
estimates = []
time_source = DASHBOARD_TIME_SOURCE_FILESYSTEM
for segment in segments:
log_path = get_route_log_path(segment.get("path"))
if log_path is not None:
logged_time = _route_logged_start_time(log_path)
if logged_time is not None and _dashboard_time_is_valid(logged_time, require_recent=True):
estimates.append(logged_time.timestamp())
time_source = DASHBOARD_TIME_SOURCE_LOG
continue
segment_num = max(0, _safe_int(segment.get("num", 0), 0))
# Segment directory mtimes normally land at the end of their one-minute segment.
estimate = _segment_mtime(segment.get("path")) - (segment_num + 1) * 60
@@ -1004,7 +1013,11 @@ def _estimate_route_started_at(segments):
if parsed is not None:
estimates.append(parsed.timestamp())
# Dashboard analysis can touch a segment directory later, but cannot make it older.
return datetime.fromtimestamp(min(estimates)) if estimates else None
return (datetime.fromtimestamp(min(estimates)), time_source) if estimates else (None, "")
def _estimate_route_started_at(segments):
return _estimate_route_start_details(segments)[0]
def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
@@ -1049,7 +1062,7 @@ def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
if not segments:
continue
started_at = _estimate_route_started_at(segments)
started_at, time_source = _estimate_route_start_details(segments)
route_infos.append({
"name": route["name"],
@@ -1057,7 +1070,7 @@ def _list_dashboard_routes(footage_paths, limit=DASHBOARD_ROUTE_SCAN_LIMIT):
"segmentCount": len(segments),
"startedAt": started_at,
"modifiedAt": route["modified_at"],
"timeSource": DASHBOARD_TIME_SOURCE_FILESYSTEM if started_at is not None else "",
"timeSource": time_source,
})
route_infos.sort(key=lambda route: (
@@ -2901,6 +2914,37 @@ def get_route_log_path(path):
return None
def _route_logged_start_time(log_path, reader=None):
"""Recover a route's wall-clock start when filesystem time was reset at boot."""
try:
if reader is None:
from openpilot.tools.lib.logreader import _LogFileReader
reader = _LogFileReader(str(log_path))
first_mono_time = None
for message in reader:
mono_time = _safe_float(getattr(message, "logMonoTime", 0), 0.0) / 1e9
if mono_time <= 0.0:
continue
first_mono_time = mono_time if first_mono_time is None else min(first_mono_time, mono_time)
message_type = _message_type(message)
payload = _message_payload(message, message_type)
wall_time = _wall_time_seconds_from_payload(payload)
if wall_time is None:
wall_time = _wall_time_seconds_from_payload(message)
if wall_time is None:
continue
start_seconds = wall_time - (mono_time - first_mono_time)
start_time = datetime.fromtimestamp(start_seconds)
if _dashboard_time_is_valid(start_time):
return start_time
except Exception:
return None
return None
def get_route_start_time(path):
log_path = get_route_log_path(path)
if log_path is None:
@@ -2917,6 +2961,10 @@ def get_route_start_time(path):
if modified_time <= 0:
return None
logged_time = _route_logged_start_time(log_path)
if logged_time is not None:
return logged_time
return datetime.fromtimestamp(modified_time)
def get_routes_names(footage_path):
+7
View File
@@ -85,6 +85,11 @@ def snapshot(allow_existing=False, include_front=None):
set_offroad_alert("Offroad_IsTakingSnapshot", True)
time.sleep(2.0) # Give hardwared time to read the param, or if just started give camerad time to start
if not params.get_bool("IsOffroad"):
params.put_bool("IsTakingSnapshot", False)
set_offroad_alert("Offroad_IsTakingSnapshot", False)
return None, None
# Check if camerad is already started
camerad_already_running = False
try:
@@ -106,6 +111,8 @@ def snapshot(allow_existing=False, include_front=None):
frame = "wideRoadCameraState"
front_frame = "driverCameraState" if front_camera_allowed else None
rear, front = get_snapshots(frame, front_frame)
if not params.get_bool("IsOffroad"):
rear, front = None, None
finally:
if not camerad_already_running:
managed_processes['camerad'].stop()
+1 -1
View File
@@ -69,7 +69,7 @@ def sensord_run(started: bool, params: Params, CP: car.CarParams, starpilot_togg
return started or params.get_bool("SentryModeEnabled")
def camera_run(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: SimpleNamespace) -> bool:
return driverview(started, params, CP, starpilot_toggles) or params.get_bool("SentryModeCapture")
return driverview(started, params, CP, starpilot_toggles) or (not started and params.get_bool("SentryModeCapture"))
def livestream(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: SimpleNamespace) -> bool:
return params.get_bool("IsLiveStreaming")
+36 -1
View File
@@ -3,7 +3,7 @@ from types import SimpleNamespace
import pytest
from cereal import car
from openpilot.system.manager.process_config import allow_uploads, managed_processes
from openpilot.system.manager.process_config import allow_uploads, camera_run, managed_processes, sentry_mode
class FakeParams:
@@ -36,3 +36,38 @@ def test_allow_uploads(started, no_uploads, no_onroad_uploads, always_allow_uplo
def test_uploader_runs_at_background_priority():
assert managed_processes["uploader"].nice == 19
class CameraParams:
def __init__(self, capture: bool):
self.capture = capture
def get_bool(self, key: str) -> bool:
assert key in {"IsDriverViewEnabled", "SentryModeCapture"}
return self.capture if key == "SentryModeCapture" else False
@pytest.mark.parametrize(
"started,capture,expected",
[
(False, True, True),
(False, False, False),
(True, False, True),
],
)
def test_camera_run_preserves_onroad_camera_and_offroad_sentry_capture(started, capture, expected):
assert camera_run(started, CameraParams(capture), car.CarParams.new_message(), SimpleNamespace()) is expected
class SentryParams:
def __init__(self, enabled: bool):
self.enabled = enabled
def get_bool(self, key: str) -> bool:
assert key == "SentryModeEnabled"
return self.enabled
@pytest.mark.parametrize("started,enabled,expected", [(True, True, False), (False, True, True), (False, False, False)])
def test_sentry_process_is_offroad_only(started, enabled, expected):
assert sentry_mode(started, SentryParams(enabled), car.CarParams.new_message(), SimpleNamespace()) is expected
+18 -1
View File
@@ -49,7 +49,7 @@ def _utc_now() -> str:
class SentryMode:
def __init__(self, params: Params | None = None, sm=None, clock=time.monotonic):
self.params = params or Params(return_defaults=True)
self.sm = sm or messaging.SubMaster(["accelerometer"])
self.sm = sm if sm is not None else messaging.SubMaster(["accelerometer", "deviceState"])
self.clock = clock
self.detector = MotionDetector(
sensitivity=SENSITIVITY,
@@ -63,6 +63,13 @@ class SentryMode:
self.armed = False
self._last_status = None
def _is_onroad(self) -> bool:
try:
device_state = self.sm["deviceState"]
except (KeyError, TypeError, AttributeError):
return False
return device_state is not None and bool(getattr(device_state, "started", False))
def _write_status(self, state: str, **extra) -> None:
status_values = {"state": state, **extra}
if status_values == self._last_status:
@@ -132,6 +139,9 @@ class SentryMode:
threading.Thread(target=publish, name="sentryd-galaxy-publish", daemon=True).start()
def _handle_detection(self, kind: str) -> None:
if self._is_onroad():
return
event_id = f"{int(time.time())}-{uuid4().hex[:8]}"
event = {
"eventId": event_id,
@@ -147,6 +157,10 @@ class SentryMode:
self._publish_event(event)
def update(self) -> None:
if self._is_onroad():
self._write_status("disabled", reason="onroad")
return
now = self.clock()
if now - self.started_at < ARM_DELAY_SECONDS:
self._write_status("arming", secondsRemaining=max(0, int(ARM_DELAY_SECONDS - (now - self.started_at))))
@@ -174,6 +188,9 @@ class SentryMode:
self._write_status("starting")
while self.params.get_bool("SentryModeEnabled"):
self.sm.update(0)
if self._is_onroad():
self._write_status("disabled", reason="onroad")
break
self.update()
time.sleep(LOOP_INTERVAL_SECONDS)
self._write_status("disabled")