diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index 47da32102..863810f81 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -31,7 +31,7 @@ void logger_rotate(LoggerdState *s) { void rotate_if_needed(LoggerdState *s) { // all encoders ready, trigger rotation - bool all_ready = s->ready_to_rotate == s->max_waiting; + bool all_ready = s->max_waiting > 0 && s->ready_to_rotate == s->max_waiting; // fallback logic to prevent extremely long segments in the case of camera, encoder, etc. malfunctions bool timed_out = false; @@ -53,6 +53,22 @@ void rotate_if_needed(LoggerdState *s) { } } +template +int get_expected_encoder_count(const LogCameraInfo (&cameras)[N]) { + const auto streams = VisionIpcClient::getAvailableStreams("camerad", false); + if (streams.empty()) { + return 0; + } + + int count = 0; + for (const auto &cam : cameras) { + if (streams.count(cam.stream_type) > 0) { + count += cam.encoder_infos.size(); + } + } + return count; +} + struct RemoteEncoder { std::unique_ptr writer; int encoderd_segment_offset; @@ -262,9 +278,9 @@ void loggerd_thread() { for (const auto &cam : cameras_logged) { for (const auto &encoder_info : cam.encoder_infos) { encoder_infos_dict[encoder_info.publish_name] = encoder_info; - s.max_waiting++; } } + s.max_waiting = get_expected_encoder_count(cameras_logged); for (auto &[sock, service] : service_state) { auto it = encoder_infos_dict.find(service.name); @@ -305,6 +321,9 @@ void loggerd_thread() { } if (service.encoder) { + if (s.max_waiting == 0) { + s.max_waiting = get_expected_encoder_count(cameras_logged); + } s.last_camera_seen_tms = millis_since_boot(); bytes_count += handle_encoder_msg(&s, msg, service.name, remote_encoders[sock], encoder_infos_dict[service.name]); } else { diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 9703ac2f5..b9c4c9590 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -97,12 +97,12 @@ class TestLoggerd: return sent_msgs - def _publish_camera_and_audio_messages(self, num_segs=1, segment_length=5): + def _publish_camera_and_audio_messages(self, num_segs=1, segment_length=5, streams=None): # Use small frame sizes for testing (width, height, size, stride, uv_offset) # NV12 format: size = stride * height * 1.5, uv_offset = stride * height w, h = 320, 240 frame_spec = (w, h, w * h * 3 // 2, w, w * h) - streams = [ + streams = streams or [ (VisionStreamType.VISION_STREAM_ROAD, frame_spec, "roadCameraState"), (VisionStreamType.VISION_STREAM_DRIVER, frame_spec, "driverCameraState"), (VisionStreamType.VISION_STREAM_WIDE_ROAD, frame_spec, "wideRoadCameraState"), @@ -204,6 +204,30 @@ class TestLoggerd: diff = logged ^ expected_files assert len(diff) == 0, f"didn't get all expected files. seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}" + @pytest.mark.xdist_group("camera_encoder_tests") + def test_rotation_without_wide_camera(self): + Params().put("RecordFront", True) + + w, h = 320, 240 + frame_spec = (w, h, w * h * 3 // 2, w, w * h) + streams = [ + (VisionStreamType.VISION_STREAM_ROAD, frame_spec, "roadCameraState"), + (VisionStreamType.VISION_STREAM_DRIVER, frame_spec, "driverCameraState"), + ] + expected_files = {"rlog.zst", "qlog.zst", "qcamera.ts", "fcamera.hevc", "dcamera.hevc"} + + num_segs = random.randint(2, 3) + length = random.randint(4, 5) + + self._publish_camera_and_audio_messages(num_segs=num_segs, segment_length=length, streams=streams) + + route_path = str(self._get_latest_log_dir()).rsplit("--", 1)[0] + for n in range(num_segs): + p = Path(f"{route_path}--{n}") + logged = {f.name for f in p.iterdir() if f.is_file()} + diff = logged ^ expected_files + assert len(diff) == 0, f"unexpected files without wide camera. seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}" + def test_bootlog(self): # generate bootlog with fake launch log launch_log = ''.join(str(random.choice(string.printable)) for _ in range(100))