From 073ce3afe30967fdf5eb62e6a5b09341394eee60 Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Fri, 28 Aug 2026 16:54:26 +0300 Subject: [PATCH 01/12] Give a Linux video frame the shape the other cameras give `LinuxVideo` emitted a dict of arrays. Every other camera driver emits a `NumpySMAdapter`, and that is what `Serializers.camera_images` reads, so a station recording a camera on this driver raised `AttributeError` and wrote no episode. The frame the headset shows reads the same field. The branch that decodes several frames out of one packet wrote them into a dict that was still `None`, and raised `TypeError`. Frames now leave one at a time, each on the port, which is what a consumer of a single image expects. Verified against a RealSense D405 over UVC: 640x480 at 27.8 Hz, and an episode of 91 frames that `load_dataset` opens. --- positronic/drivers/camera/linux_video.py | 37 +++--- positronic/drivers/camera/tests/conftest.py | 30 +++++ .../drivers/camera/tests/test_linux_video.py | 123 ++++++++++++++++++ 3 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 positronic/drivers/camera/tests/conftest.py create mode 100644 positronic/drivers/camera/tests/test_linux_video.py diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index c982f10bc..7364736d3 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -19,9 +19,10 @@ def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_fo self.fps = fps self.pixel_format = pixel_format self.fps_counter = pimm.utils.RateCounter(f'LinuxVideo {device_path}') - self.frame: pimm.SignalEmitter = pimm.ControlSystemEmitter(self) + self.frame = pimm.ControlSystemEmitter[pimm.shared_memory.NumpySMAdapter](self) + self._frame_adapter = None # Lazy init - def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]: # noqa: C901 + def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]: codec_mapping = { PixelFormat.H264: 'h264', PixelFormat.HEVC: 'hevc', @@ -49,33 +50,31 @@ def get_codec_context(codec_name: str) -> av.CodecContext: break data = np.frombuffer(frame.data, dtype=np.uint8) - result = None match frame.pixel_format: case PixelFormat.YUYV: data = data.reshape((frame.height, frame.width, 2)) - result = {'image': cv2.cvtColor(data, cv2.COLOR_YUV2RGB_YUYV)} + images = [cv2.cvtColor(data, cv2.COLOR_YUV2RGB_YUYV)] case PixelFormat.UYVY: data = data.reshape((frame.height, frame.width, 2)) - result = {'image': cv2.cvtColor(data, cv2.COLOR_YUV2RGB_UYVY)} + images = [cv2.cvtColor(data, cv2.COLOR_YUV2RGB_UYVY)] case _ if frame.pixel_format in codec_mapping: - codec_name = codec_mapping[frame.pixel_format] - codec_ctx = get_codec_context(codec_name) - packets = codec_ctx.parse(data) - for packet in packets: - frames = codec_ctx.decode(packet) - if len(frames) == 1: - result = {'image': frames[0].to_ndarray(format='rgb24')} - else: - for i, decoded_frame in enumerate(frames): - result[f'image_{i}'] = decoded_frame.to_ndarray(format='rgb24') + codec_ctx = get_codec_context(codec_mapping[frame.pixel_format]) + # `av` types what it parses as `bytes` and carries `decode` on the subclasses of + # `CodecContext`, so the buffer the device hands over and the base class both read wrong. + packets = codec_ctx.parse(data) # pyright: ignore[reportArgumentType] + images = [ + decoded.to_ndarray(format='rgb24') + for packet in packets + for decoded in codec_ctx.decode(packet) # pyright: ignore[reportAttributeAccessIssue] + ] case _: # Assume 3 bytes per pixel (RGB/BGR) - rgb_data = data.reshape((frame.height, frame.width, 3)) - result = {'image': rgb_data} + images = [data.reshape((frame.height, frame.width, 3))] - if result is not None: - self.frame.emit(result) + for image in images: + self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(image, self._frame_adapter) + self.frame.emit(self._frame_adapter) self.fps_counter.tick() yield pimm.Yield() # Give control back to the world diff --git a/positronic/drivers/camera/tests/conftest.py b/positronic/drivers/camera/tests/conftest.py new file mode 100644 index 000000000..6e5954c0f --- /dev/null +++ b/positronic/drivers/camera/tests/conftest.py @@ -0,0 +1,30 @@ +"""A stand-in for the vendor package the Linux video driver imports. + +``linuxpy`` ships only in the ``hardware`` extra, so the driver module cannot be imported from a default +sync. The tests drive a device of their own, so a module carrying the names the driver binds at import is +enough. Installed here, before any test module imports the driver, and only where the real package is +absent. +""" + +import importlib.util +import sys +import types +from enum import Enum + +VENDOR = 'linuxpy' +DEVICE_MODULE = f'{VENDOR}.video.device' + +if importlib.util.find_spec(VENDOR) is None: + # The formats the driver names. The values are the V4L2 four-character codes, as `linuxpy` reports them. + pixel_format = Enum('PixelFormat', ['YUYV', 'UYVY', 'RGB24', 'H264', 'HEVC', 'VP8', 'VP9', 'MPEG4', 'MJPEG']) + + device = types.ModuleType(DEVICE_MODULE) + device.__dict__.update(Device=object, PixelFormat=pixel_format) + + video = types.ModuleType(f'{VENDOR}.video') + video.__dict__.update(device=device) + + package = types.ModuleType(VENDOR) + package.__dict__.update(video=video) + + sys.modules.update({VENDOR: package, f'{VENDOR}.video': video, DEVICE_MODULE: device}) diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py new file mode 100644 index 000000000..7e90efcae --- /dev/null +++ b/positronic/drivers/camera/tests/test_linux_video.py @@ -0,0 +1,123 @@ +"""What the Linux video driver puts on its frame port, from the buffers a device hands it.""" + +import numpy as np +import pytest + +import pimm +from positronic.drivers.camera import linux_video +from positronic.tests.testing_coutils import RecordingEmitter + +WIDTH, HEIGHT = 4, 2 + + +class StopFlag(pimm.SignalReceiver[bool]): + """``should_stop`` under the test's control.""" + + def __init__(self): + self.stopped = False + + def read(self) -> pimm.Message[bool]: + return pimm.Message(self.stopped) + + +class FakeFrame: + def __init__(self, data: bytes, pixel_format): + self.data = data + self.pixel_format = pixel_format + self.width, self.height = WIDTH, HEIGHT + + +class FakeDevice: + """A device that hands over the frames a test gives it, and records what it was set to.""" + + to_serve: list['FakeFrame'] = [] + opened: 'FakeDevice | None' = None + + def __init__(self, path: str): + self.path = path + self.info = type('Info', (), {'buffers': ['capture']})() + self.frames = list(FakeDevice.to_serve) + self.format = None + self.fps = None + self.closed = False + FakeDevice.opened = self + + def open(self) -> None: + pass + + def set_format(self, buffer, width, height, pixel_format) -> None: + self.format = (buffer, width, height, pixel_format) + + def set_fps(self, buffer, fps) -> None: + self.fps = (buffer, fps) + + def __iter__(self): + return iter(self.frames) + + def close(self) -> None: + self.closed = True + + +@pytest.fixture +def device(monkeypatch): + monkeypatch.setattr(linux_video, 'Device', FakeDevice) + return FakeDevice + + +def _driven(frames, **kwargs): + """A driver over a device carrying ``frames``, with its port recorded, run to exhaustion.""" + camera = linux_video.LinuxVideo( + device_path='/dev/null', width=WIDTH, height=HEIGHT, fps=30, pixel_format='YUYV', **kwargs + ) + emitted = RecordingEmitter() + camera.frame._bind(emitted) + FakeDevice.to_serve = frames + list(camera.run(StopFlag(), pimm.world.SystemClock())) + opened = FakeDevice.opened + assert opened is not None, 'the driver opened no device' + return emitted, opened + + +def _yuyv(luma: int) -> bytes: + """One YUYV buffer of a flat grey, which converts to a flat grey RGB image.""" + return bytes([luma, 128] * (WIDTH * HEIGHT)) + + +def test_a_yuyv_buffer_reaches_the_port_as_an_image(device): + emitted, _ = _driven([FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV)]) + + assert len(emitted.emitted) == 1 + _, adapter = emitted.emitted[0] + assert adapter.array.shape == (HEIGHT, WIDTH, 3) + assert adapter.array.dtype == np.uint8 + assert adapter.array.min() > 150 # the grey survives the conversion + + +def test_every_buffer_is_one_frame(device): + frames = [FakeFrame(_yuyv(v), linux_video.PixelFormat.YUYV) for v in (50, 120, 200)] + + emitted, _ = _driven(frames) + + assert len(emitted.emitted) == 3 + + +def test_the_device_is_set_to_what_the_driver_was_asked_for(device): + _, opened = _driven([FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)]) + + assert opened.format == ('capture', WIDTH, HEIGHT, 'YUYV') + assert opened.fps == ('capture', 30) + + +def test_the_device_is_closed_when_the_frames_run_out(device): + _, opened = _driven([FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)]) + + assert opened.closed + + +def test_a_buffer_of_three_bytes_a_pixel_is_taken_as_it_is(device): + raw = bytes(range(WIDTH * HEIGHT * 3)) + + emitted, _ = _driven([FakeFrame(raw, linux_video.PixelFormat.RGB24)]) + + _, adapter = emitted.emitted[0] + np.testing.assert_array_equal(adapter.array, np.frombuffer(raw, dtype=np.uint8).reshape(HEIGHT, WIDTH, 3)) From 54c1600d30c3dbca83bdc60427918c28db22438a Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Fri, 28 Aug 2026 17:06:44 +0300 Subject: [PATCH 02/12] Drop a video buffer that arrives short of a frame A camera hands over a buffer with its tail missing when the bus is busy, and the driver reshaped it and raised `ValueError`, which ends the run. Four D405 cameras on one USB 3 hub are enough to see it: the first minute of the first run raised on a buffer of 312072 bytes where a 640x480 frame is 614400. A short buffer now goes, and the driver says so once and counts the rest. Over three minutes of four cameras at 30 Hz that is one buffer per camera; while the same four are recorded, and the machine encodes their video, it is five to eight per camera per minute. Reading a buffer moves out of `run` into `_images`, which is what the whole `match` was, and what put `run` over the complexity the linter allows. --- positronic/drivers/camera/linux_video.py | 89 ++++++++++++------- .../drivers/camera/tests/test_linux_video.py | 12 +++ 2 files changed, 70 insertions(+), 31 deletions(-) diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index 7364736d3..bfcad0046 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -1,3 +1,4 @@ +import logging from collections.abc import Iterator import av @@ -10,6 +11,18 @@ with vendor_import('linuxpy', 'Linux video capture', platforms=('linux',)): from linuxpy.video.device import Device, PixelFormat +logger = logging.getLogger(__name__) + +# The formats a camera may compress in, and what decodes each of them. +_CODECS = { + PixelFormat.H264: 'h264', + PixelFormat.HEVC: 'hevc', + PixelFormat.VP8: 'vp8', + PixelFormat.VP9: 'vp9', + PixelFormat.MPEG4: 'mpeg4', + PixelFormat.MJPEG: 'mjpeg', +} + class LinuxVideo(pimm.ControlSystem): def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_format: str): @@ -22,18 +35,46 @@ def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_fo self.frame = pimm.ControlSystemEmitter[pimm.shared_memory.NumpySMAdapter](self) self._frame_adapter = None # Lazy init + @staticmethod + def _framed(data: np.ndarray, frame, bytes_per_pixel: int) -> np.ndarray | None: + """``data`` shaped as the frame it belongs to, or ``None`` where the buffer arrived short. + + A camera hands over a buffer with its tail missing when the bus is busy, and several cameras on one + controller are enough to see it. A partial frame has nothing to read, and the next one is a + thirtieth of a second away. + """ + if data.size != frame.height * frame.width * bytes_per_pixel: + return None + return data.reshape((frame.height, frame.width, bytes_per_pixel)) + + def _images(self, frame, codec_context) -> list[np.ndarray]: + """Every image the buffer ``frame`` carries, as RGB. Empty where the buffer arrived short.""" + data = np.frombuffer(frame.data, dtype=np.uint8) + match frame.pixel_format: + case PixelFormat.YUYV: + raw = self._framed(data, frame, 2) + return [] if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_YUYV)] + case PixelFormat.UYVY: + raw = self._framed(data, frame, 2) + return [] if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_UYVY)] + case _ if frame.pixel_format in _CODECS: + codec_ctx = codec_context(_CODECS[frame.pixel_format]) + # `av` types what it parses as `bytes` and carries `decode` on the subclasses of + # `CodecContext`, so the buffer the device hands over and the base class both read wrong. + packets = codec_ctx.parse(data) # pyright: ignore[reportArgumentType] + return [ + decoded.to_ndarray(format='rgb24') + for packet in packets + for decoded in codec_ctx.decode(packet) # pyright: ignore[reportAttributeAccessIssue] + ] + case _: + raw = self._framed(data, frame, 3) # assume 3 bytes per pixel (RGB/BGR) + return [] if raw is None else [raw] + def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]: - codec_mapping = { - PixelFormat.H264: 'h264', - PixelFormat.HEVC: 'hevc', - PixelFormat.VP8: 'vp8', - PixelFormat.VP9: 'vp9', - PixelFormat.MPEG4: 'mpeg4', - PixelFormat.MJPEG: 'mjpeg', - } codec_contexts = {} - def get_codec_context(codec_name: str) -> av.CodecContext: + def codec_context(codec_name: str) -> av.CodecContext: """Lazily initialize and return codec context for given codec""" if codec_name not in codec_contexts: codec_contexts[codec_name] = av.CodecContext.create(codec_name, 'r') @@ -45,32 +86,16 @@ def get_codec_context(codec_name: str) -> av.CodecContext: device.set_format(device.info.buffers[0], self.width, self.height, self.pixel_format) device.set_fps(device.info.buffers[0], self.fps) + short = 0 for frame in device: if should_stop.value: break - data = np.frombuffer(frame.data, dtype=np.uint8) - - match frame.pixel_format: - case PixelFormat.YUYV: - data = data.reshape((frame.height, frame.width, 2)) - images = [cv2.cvtColor(data, cv2.COLOR_YUV2RGB_YUYV)] - case PixelFormat.UYVY: - data = data.reshape((frame.height, frame.width, 2)) - images = [cv2.cvtColor(data, cv2.COLOR_YUV2RGB_UYVY)] - case _ if frame.pixel_format in codec_mapping: - codec_ctx = get_codec_context(codec_mapping[frame.pixel_format]) - # `av` types what it parses as `bytes` and carries `decode` on the subclasses of - # `CodecContext`, so the buffer the device hands over and the base class both read wrong. - packets = codec_ctx.parse(data) # pyright: ignore[reportArgumentType] - images = [ - decoded.to_ndarray(format='rgb24') - for packet in packets - for decoded in codec_ctx.decode(packet) # pyright: ignore[reportAttributeAccessIssue] - ] - case _: - # Assume 3 bytes per pixel (RGB/BGR) - images = [data.reshape((frame.height, frame.width, 3))] + images = self._images(frame, codec_context) + if not images: + short += 1 + if short == 1: + logger.warning('%s handed over a buffer short of a frame', self.device_path) for image in images: self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(image, self._frame_adapter) @@ -79,4 +104,6 @@ def get_codec_context(codec_name: str) -> av.CodecContext: yield pimm.Yield() # Give control back to the world + if short: + logger.warning('%s handed over %d buffers short of a frame', self.device_path, short) device.close() diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py index 7e90efcae..2557f2c51 100644 --- a/positronic/drivers/camera/tests/test_linux_video.py +++ b/positronic/drivers/camera/tests/test_linux_video.py @@ -114,6 +114,18 @@ def test_the_device_is_closed_when_the_frames_run_out(device): assert opened.closed +def test_a_buffer_short_of_a_frame_is_dropped(device, caplog): + """A busy bus hands over a buffer with its tail missing, and a run outlives it.""" + short = FakeFrame(_yuyv(200)[: WIDTH * HEIGHT], linux_video.PixelFormat.YUYV) + whole = FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV) + + emitted, _ = _driven([short, whole, short]) + + assert len(emitted.emitted) == 1 + assert 'short of a frame' in caplog.text + assert 'handed over 2 buffers' in caplog.text + + def test_a_buffer_of_three_bytes_a_pixel_is_taken_as_it_is(device): raw = bytes(range(WIDTH * HEIGHT * 3)) From 467aa1fb11cf4ff551c798150a39a52b9e8d8eba Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Fri, 28 Aug 2026 18:30:11 +0300 Subject: [PATCH 03/12] Keep the video library's ioctl log out of a debug run `linuxpy` logs every ioctl, which is a handful of lines per frame per camera: two cameras at 30 Hz bury everything else a debug run has to say. It joins the libraries whose level is pinned. --- pimm/logging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pimm/logging.py b/pimm/logging.py index 99d628a69..748337053 100644 --- a/pimm/logging.py +++ b/pimm/logging.py @@ -39,6 +39,7 @@ 'boto3', 's3transfer', # per part of a multipart upload 'asyncio', # per selector event, under its debug mode + 'linuxpy', # per ioctl, which is several times a frame for every camera ) From 38505cab379e78fc637108a72b2abc176fe3ee58 Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Thu, 3 Sep 2026 19:11:56 +0300 Subject: [PATCH 04/12] Tell a decoder holding a frame back from a buffer short of one Four things the review of this branch found. A buffer that decodes to several images emitted them all through one adapter, overwriting it between emissions and with nothing read in between, so every message of that batch carried the last image. Measured on a four-frame H.264 stream: three emissions, all of them the third image. Such a buffer now gives each image an adapter of its own; the one-image buffer every uncompressed camera hands over still reuses the driver's. `short` counted every buffer that yielded no image, but a parser and a decoder both hold whole data back until they have a frame to give: the first 64 bytes of an H.264 stream are healthy and decode to nothing. `_images` now returns `None` for a buffer short of a frame and an empty list for a wait, and only the former is counted or logged. `_framed` said in its docstring which rig the short buffers were seen on and assumed 30 fps; it now states the contract and stops there. `_driven` in the tests worked only after the `device` fixture had patched the driver, and said so nowhere -- a test that called it without the otherwise unused fixture would have opened the real device. It takes the device now. --- positronic/drivers/camera/linux_video.py | 34 ++++++----- .../drivers/camera/tests/test_linux_video.py | 61 ++++++++++++++++--- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index bfcad0046..d3a25e268 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -37,26 +37,25 @@ def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_fo @staticmethod def _framed(data: np.ndarray, frame, bytes_per_pixel: int) -> np.ndarray | None: - """``data`` shaped as the frame it belongs to, or ``None`` where the buffer arrived short. - - A camera hands over a buffer with its tail missing when the bus is busy, and several cameras on one - controller are enough to see it. A partial frame has nothing to read, and the next one is a - thirtieth of a second away. - """ + """``data`` shaped as the frame it belongs to, or ``None`` where it is short of one.""" if data.size != frame.height * frame.width * bytes_per_pixel: return None return data.reshape((frame.height, frame.width, bytes_per_pixel)) - def _images(self, frame, codec_context) -> list[np.ndarray]: - """Every image the buffer ``frame`` carries, as RGB. Empty where the buffer arrived short.""" + def _images(self, frame, codec_context) -> list[np.ndarray] | None: + """Every image the buffer ``frame`` carries, as RGB, or ``None`` where it is short of a frame. + + A compressed buffer whole enough to read may still carry no image: the parser and the decoder both + hold data back until they have a frame to give, so an empty list is a wait, not a loss. + """ data = np.frombuffer(frame.data, dtype=np.uint8) match frame.pixel_format: case PixelFormat.YUYV: raw = self._framed(data, frame, 2) - return [] if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_YUYV)] + return None if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_YUYV)] case PixelFormat.UYVY: raw = self._framed(data, frame, 2) - return [] if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_UYVY)] + return None if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_UYVY)] case _ if frame.pixel_format in _CODECS: codec_ctx = codec_context(_CODECS[frame.pixel_format]) # `av` types what it parses as `bytes` and carries `decode` on the subclasses of @@ -69,7 +68,7 @@ def _images(self, frame, codec_context) -> list[np.ndarray]: ] case _: raw = self._framed(data, frame, 3) # assume 3 bytes per pixel (RGB/BGR) - return [] if raw is None else [raw] + return None if raw is None else [raw] def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]: codec_contexts = {} @@ -92,15 +91,20 @@ def codec_context(codec_name: str) -> av.CodecContext: break images = self._images(frame, codec_context) - if not images: + if images is None: short += 1 if short == 1: logger.warning('%s handed over a buffer short of a frame', self.device_path) - - for image in images: - self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(image, self._frame_adapter) + elif len(images) == 1: + self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(images[0], self._frame_adapter) self.frame.emit(self._frame_adapter) self.fps_counter.tick() + else: + # A buffer that decodes to several images emits them with nothing read in between, so one + # adapter shared between them would show every reader the last image. + for image in images: + self.frame.emit(pimm.shared_memory.NumpySMAdapter.lazy_init(image, None)) + self.fps_counter.tick() yield pimm.Yield() # Give control back to the world diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py index 2557f2c51..243bcb1a7 100644 --- a/positronic/drivers/camera/tests/test_linux_video.py +++ b/positronic/drivers/camera/tests/test_linux_video.py @@ -1,5 +1,8 @@ """What the Linux video driver puts on its frame port, from the buffers a device hands it.""" +import io + +import av import numpy as np import pytest @@ -60,20 +63,22 @@ def close(self) -> None: @pytest.fixture def device(monkeypatch): + """The class the driver opens, replaced by one a test hands frames to. Pass it to ``_driven``.""" monkeypatch.setattr(linux_video, 'Device', FakeDevice) + FakeDevice.to_serve, FakeDevice.opened = [], None return FakeDevice -def _driven(frames, **kwargs): - """A driver over a device carrying ``frames``, with its port recorded, run to exhaustion.""" +def _driven(device, frames, **kwargs): + """A driver over ``device`` carrying ``frames``, with its port recorded, run to exhaustion.""" camera = linux_video.LinuxVideo( device_path='/dev/null', width=WIDTH, height=HEIGHT, fps=30, pixel_format='YUYV', **kwargs ) emitted = RecordingEmitter() camera.frame._bind(emitted) - FakeDevice.to_serve = frames + device.to_serve = frames list(camera.run(StopFlag(), pimm.world.SystemClock())) - opened = FakeDevice.opened + opened = device.opened assert opened is not None, 'the driver opened no device' return emitted, opened @@ -84,7 +89,7 @@ def _yuyv(luma: int) -> bytes: def test_a_yuyv_buffer_reaches_the_port_as_an_image(device): - emitted, _ = _driven([FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV)]) + emitted, _ = _driven(device, [FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV)]) assert len(emitted.emitted) == 1 _, adapter = emitted.emitted[0] @@ -96,20 +101,20 @@ def test_a_yuyv_buffer_reaches_the_port_as_an_image(device): def test_every_buffer_is_one_frame(device): frames = [FakeFrame(_yuyv(v), linux_video.PixelFormat.YUYV) for v in (50, 120, 200)] - emitted, _ = _driven(frames) + emitted, _ = _driven(device, frames) assert len(emitted.emitted) == 3 def test_the_device_is_set_to_what_the_driver_was_asked_for(device): - _, opened = _driven([FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)]) + _, opened = _driven(device, [FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)]) assert opened.format == ('capture', WIDTH, HEIGHT, 'YUYV') assert opened.fps == ('capture', 30) def test_the_device_is_closed_when_the_frames_run_out(device): - _, opened = _driven([FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)]) + _, opened = _driven(device, [FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)]) assert opened.closed @@ -119,7 +124,7 @@ def test_a_buffer_short_of_a_frame_is_dropped(device, caplog): short = FakeFrame(_yuyv(200)[: WIDTH * HEIGHT], linux_video.PixelFormat.YUYV) whole = FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV) - emitted, _ = _driven([short, whole, short]) + emitted, _ = _driven(device, [short, whole, short]) assert len(emitted.emitted) == 1 assert 'short of a frame' in caplog.text @@ -129,7 +134,43 @@ def test_a_buffer_short_of_a_frame_is_dropped(device, caplog): def test_a_buffer_of_three_bytes_a_pixel_is_taken_as_it_is(device): raw = bytes(range(WIDTH * HEIGHT * 3)) - emitted, _ = _driven([FakeFrame(raw, linux_video.PixelFormat.RGB24)]) + emitted, _ = _driven(device, [FakeFrame(raw, linux_video.PixelFormat.RGB24)]) _, adapter = emitted.emitted[0] np.testing.assert_array_equal(adapter.array, np.frombuffer(raw, dtype=np.uint8).reshape(HEIGHT, WIDTH, 3)) + + +def _h264(count: int) -> bytes: + """An H.264 stream of ``count`` frames, as a camera that compresses would hand it over.""" + stream = io.BytesIO() + container = av.open(stream, 'w', format='h264') + encoded = container.add_stream('libx264', rate=30) + encoded.width, encoded.height, encoded.pix_fmt = WIDTH * 16, HEIGHT * 16, 'yuv420p' + encoded.options = {'tune': 'zerolatency'} # a camera streams frames in order, and so must the fixture + for i in range(count): + image = np.full((HEIGHT * 16, WIDTH * 16, 3), i * 40, np.uint8) + for packet in encoded.encode(av.VideoFrame.from_ndarray(image, format='rgb24')): + container.mux(packet) + for packet in encoded.encode(None): + container.mux(packet) + container.close() + return stream.getvalue() + + +def test_a_compressed_buffer_the_decoder_holds_back_is_not_counted_short(device, caplog): + """The decoder gives no image until it has one, and a whole buffer is not a truncated one.""" + head = FakeFrame(_h264(4)[:64], linux_video.PixelFormat.H264) + + emitted, _ = _driven(device, [head]) + + assert emitted.emitted == [] + assert 'short of a frame' not in caplog.text + + +def test_every_image_of_one_buffer_keeps_its_own_pixels(device): + """One buffer decoding to several images must not hand the same adapter, and its pixels, to each.""" + emitted, _ = _driven(device, [FakeFrame(_h264(4), linux_video.PixelFormat.H264)]) + + greys = [adapter.array.mean() for _, adapter in emitted.emitted] + assert len(greys) > 1, 'the buffer decoded to a single image, so nothing was shared' + assert len({round(grey) for grey in greys}) == len(greys), f'images share their pixels: {greys}' From 852f53d236ac361bdedde4c6016f5fd6898a3a7b Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Mon, 7 Sep 2026 19:25:24 +0300 Subject: [PATCH 05/12] Name the buffer a camera hands over for its size, not its tail Three things the second review round of this branch found. `short` claimed truncation, but `_framed` refuses every buffer whose size is not one frame's -- an oversized or padded one as much as a truncated one. The counter, both warnings and the two tests that read them now say the size is wrong and stop there. `_CODECS` sat at module scope with `LinuxVideo._images` its only reader; it is a class attribute beside that method now. `test_every_buffer_is_one_frame` claimed of every buffer what the compressed tests in the same file disprove: it is about a whole YUYV buffer, and says so. The fake `linuxpy` enum was bound as `pixel_format` and installed as `PixelFormat`; it carries the one name now. --- positronic/drivers/camera/linux_video.py | 40 +++++++++---------- positronic/drivers/camera/tests/conftest.py | 4 +- .../drivers/camera/tests/test_linux_video.py | 16 ++++---- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index d3a25e268..ac363cb40 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -13,18 +13,18 @@ logger = logging.getLogger(__name__) -# The formats a camera may compress in, and what decodes each of them. -_CODECS = { - PixelFormat.H264: 'h264', - PixelFormat.HEVC: 'hevc', - PixelFormat.VP8: 'vp8', - PixelFormat.VP9: 'vp9', - PixelFormat.MPEG4: 'mpeg4', - PixelFormat.MJPEG: 'mjpeg', -} - class LinuxVideo(pimm.ControlSystem): + # The formats a camera may compress in, and what decodes each of them. + _CODECS = { + PixelFormat.H264: 'h264', + PixelFormat.HEVC: 'hevc', + PixelFormat.VP8: 'vp8', + PixelFormat.VP9: 'vp9', + PixelFormat.MPEG4: 'mpeg4', + PixelFormat.MJPEG: 'mjpeg', + } + def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_format: str): self.device_path = device_path self.width = width @@ -37,13 +37,13 @@ def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_fo @staticmethod def _framed(data: np.ndarray, frame, bytes_per_pixel: int) -> np.ndarray | None: - """``data`` shaped as the frame it belongs to, or ``None`` where it is short of one.""" + """``data`` shaped as the frame it belongs to, or ``None`` where it is not one frame's worth.""" if data.size != frame.height * frame.width * bytes_per_pixel: return None return data.reshape((frame.height, frame.width, bytes_per_pixel)) def _images(self, frame, codec_context) -> list[np.ndarray] | None: - """Every image the buffer ``frame`` carries, as RGB, or ``None`` where it is short of a frame. + """Every image the buffer ``frame`` carries, as RGB, or ``None`` where it is not one frame's worth. A compressed buffer whole enough to read may still carry no image: the parser and the decoder both hold data back until they have a frame to give, so an empty list is a wait, not a loss. @@ -56,8 +56,8 @@ def _images(self, frame, codec_context) -> list[np.ndarray] | None: case PixelFormat.UYVY: raw = self._framed(data, frame, 2) return None if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_UYVY)] - case _ if frame.pixel_format in _CODECS: - codec_ctx = codec_context(_CODECS[frame.pixel_format]) + case _ if frame.pixel_format in self._CODECS: + codec_ctx = codec_context(self._CODECS[frame.pixel_format]) # `av` types what it parses as `bytes` and carries `decode` on the subclasses of # `CodecContext`, so the buffer the device hands over and the base class both read wrong. packets = codec_ctx.parse(data) # pyright: ignore[reportArgumentType] @@ -85,16 +85,16 @@ def codec_context(codec_name: str) -> av.CodecContext: device.set_format(device.info.buffers[0], self.width, self.height, self.pixel_format) device.set_fps(device.info.buffers[0], self.fps) - short = 0 + misframed = 0 for frame in device: if should_stop.value: break images = self._images(frame, codec_context) if images is None: - short += 1 - if short == 1: - logger.warning('%s handed over a buffer short of a frame', self.device_path) + misframed += 1 + if misframed == 1: + logger.warning('%s handed over a buffer that is not one frame in size', self.device_path) elif len(images) == 1: self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(images[0], self._frame_adapter) self.frame.emit(self._frame_adapter) @@ -108,6 +108,6 @@ def codec_context(codec_name: str) -> av.CodecContext: yield pimm.Yield() # Give control back to the world - if short: - logger.warning('%s handed over %d buffers short of a frame', self.device_path, short) + if misframed: + logger.warning('%s handed over %d buffers that are not one frame in size', self.device_path, misframed) device.close() diff --git a/positronic/drivers/camera/tests/conftest.py b/positronic/drivers/camera/tests/conftest.py index 6e5954c0f..b2c8876c8 100644 --- a/positronic/drivers/camera/tests/conftest.py +++ b/positronic/drivers/camera/tests/conftest.py @@ -16,10 +16,10 @@ if importlib.util.find_spec(VENDOR) is None: # The formats the driver names. The values are the V4L2 four-character codes, as `linuxpy` reports them. - pixel_format = Enum('PixelFormat', ['YUYV', 'UYVY', 'RGB24', 'H264', 'HEVC', 'VP8', 'VP9', 'MPEG4', 'MJPEG']) + PixelFormat = Enum('PixelFormat', ['YUYV', 'UYVY', 'RGB24', 'H264', 'HEVC', 'VP8', 'VP9', 'MPEG4', 'MJPEG']) device = types.ModuleType(DEVICE_MODULE) - device.__dict__.update(Device=object, PixelFormat=pixel_format) + device.__dict__.update(Device=object, PixelFormat=PixelFormat) video = types.ModuleType(f'{VENDOR}.video') video.__dict__.update(device=device) diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py index 243bcb1a7..eb19ea63c 100644 --- a/positronic/drivers/camera/tests/test_linux_video.py +++ b/positronic/drivers/camera/tests/test_linux_video.py @@ -98,7 +98,7 @@ def test_a_yuyv_buffer_reaches_the_port_as_an_image(device): assert adapter.array.min() > 150 # the grey survives the conversion -def test_every_buffer_is_one_frame(device): +def test_every_whole_yuyv_buffer_is_one_frame(device): frames = [FakeFrame(_yuyv(v), linux_video.PixelFormat.YUYV) for v in (50, 120, 200)] emitted, _ = _driven(device, frames) @@ -119,15 +119,15 @@ def test_the_device_is_closed_when_the_frames_run_out(device): assert opened.closed -def test_a_buffer_short_of_a_frame_is_dropped(device, caplog): - """A busy bus hands over a buffer with its tail missing, and a run outlives it.""" - short = FakeFrame(_yuyv(200)[: WIDTH * HEIGHT], linux_video.PixelFormat.YUYV) +def test_a_buffer_that_is_not_one_frame_in_size_is_dropped(device, caplog): + """A busy bus hands over a buffer that is not a whole frame, and a run outlives it.""" + misframed = FakeFrame(_yuyv(200)[: WIDTH * HEIGHT], linux_video.PixelFormat.YUYV) whole = FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV) - emitted, _ = _driven(device, [short, whole, short]) + emitted, _ = _driven(device, [misframed, whole, misframed]) assert len(emitted.emitted) == 1 - assert 'short of a frame' in caplog.text + assert 'not one frame in size' in caplog.text assert 'handed over 2 buffers' in caplog.text @@ -157,14 +157,14 @@ def _h264(count: int) -> bytes: return stream.getvalue() -def test_a_compressed_buffer_the_decoder_holds_back_is_not_counted_short(device, caplog): +def test_a_compressed_buffer_the_decoder_holds_back_is_not_counted_misframed(device, caplog): """The decoder gives no image until it has one, and a whole buffer is not a truncated one.""" head = FakeFrame(_h264(4)[:64], linux_video.PixelFormat.H264) emitted, _ = _driven(device, [head]) assert emitted.emitted == [] - assert 'short of a frame' not in caplog.text + assert 'not one frame in size' not in caplog.text def test_every_image_of_one_buffer_keeps_its_own_pixels(device): From 3eba9af9ca9c06346b0fd19784aa79f8524f55c5 Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Mon, 7 Sep 2026 19:30:20 +0300 Subject: [PATCH 06/12] Say why a camera buffer that is not a frame is counted, not raised A V4L2 buffer short of a frame is what a busy bus hands over: measured on the station's four D405, one per camera over three minutes of capture and five to eight per camera per minute while all four are also being encoded. The waiver records that, so the count is read as traffic rather than as a swallowed fault. --- positronic/drivers/camera/linux_video.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index ac363cb40..a5b0c02ba 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -91,6 +91,9 @@ def codec_context(codec_name: str) -> av.CodecContext: break images = self._images(frame, codec_context) + # rules-allow: swallowed-error — a busy bus hands over a buffer that is not a whole frame as + # ordinary traffic, roughly one per camera per minute of capture on the station's four D405; + # the next buffer is a thirtieth of a second away, and the count says how many went if images is None: misframed += 1 if misframed == 1: From 9b2bf0da836411dd202a584294fd1a589c72f000 Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Mon, 7 Sep 2026 19:42:11 +0300 Subject: [PATCH 07/12] Hand the frame port the newest image of a buffer, not all of them The port is a latest-value slot -- shared memory across a process boundary, one message in place within one -- and nothing runs between two emissions of the same tick. So every image but the last of a buffer that decodes to several was written over before any reader could see it, and `fps_counter` counted sends nobody received. Such a buffer now hands over its newest image and counts the rest, logged once at the end of the run beside the buffers of the wrong size. Only H.264 and the other compressed formats can produce one; a YUYV or UYVY buffer is a single image by construction. --- positronic/drivers/camera/linux_video.py | 17 ++++++++--------- .../drivers/camera/tests/test_linux_video.py | 16 +++++++++++----- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index a5b0c02ba..43cc6cac2 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -85,7 +85,7 @@ def codec_context(codec_name: str) -> av.CodecContext: device.set_format(device.info.buffers[0], self.width, self.height, self.pixel_format) device.set_fps(device.info.buffers[0], self.fps) - misframed = 0 + misframed = overtaken = 0 for frame in device: if should_stop.value: break @@ -98,19 +98,18 @@ def codec_context(codec_name: str) -> av.CodecContext: misframed += 1 if misframed == 1: logger.warning('%s handed over a buffer that is not one frame in size', self.device_path) - elif len(images) == 1: - self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(images[0], self._frame_adapter) + elif images: + # The port holds one image and nothing runs between two emissions of the same tick, so a + # buffer decoding to several has only its newest to give; the rest are counted, not sent. + overtaken += len(images) - 1 + self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(images[-1], self._frame_adapter) self.frame.emit(self._frame_adapter) self.fps_counter.tick() - else: - # A buffer that decodes to several images emits them with nothing read in between, so one - # adapter shared between them would show every reader the last image. - for image in images: - self.frame.emit(pimm.shared_memory.NumpySMAdapter.lazy_init(image, None)) - self.fps_counter.tick() yield pimm.Yield() # Give control back to the world if misframed: logger.warning('%s handed over %d buffers that are not one frame in size', self.device_path, misframed) + if overtaken: + logger.warning('%s decoded %d images a newer one of the same buffer overtook', self.device_path, overtaken) device.close() diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py index eb19ea63c..225f04916 100644 --- a/positronic/drivers/camera/tests/test_linux_video.py +++ b/positronic/drivers/camera/tests/test_linux_video.py @@ -167,10 +167,16 @@ def test_a_compressed_buffer_the_decoder_holds_back_is_not_counted_misframed(dev assert 'not one frame in size' not in caplog.text -def test_every_image_of_one_buffer_keeps_its_own_pixels(device): - """One buffer decoding to several images must not hand the same adapter, and its pixels, to each.""" +def test_a_buffer_of_several_images_gives_the_newest(device, caplog): + """The frame port holds one image, so the older images of a buffer have nowhere to go.""" + buffer = FakeFrame(_h264(4), linux_video.PixelFormat.H264) + driver = linux_video.LinuxVideo(device_path='/dev/null', width=WIDTH, height=HEIGHT, fps=30, pixel_format='YUYV') + decoded = driver._images(buffer, lambda name: av.CodecContext.create(name, 'r')) + assert decoded is not None and len(decoded) > 1, 'the buffer decoded to a single image' + emitted, _ = _driven(device, [FakeFrame(_h264(4), linux_video.PixelFormat.H264)]) - greys = [adapter.array.mean() for _, adapter in emitted.emitted] - assert len(greys) > 1, 'the buffer decoded to a single image, so nothing was shared' - assert len({round(grey) for grey in greys}) == len(greys), f'images share their pixels: {greys}' + assert len(emitted.emitted) == 1 + _, adapter = emitted.emitted[0] + assert adapter.array.mean() == pytest.approx(decoded[-1].mean()) + assert f'{len(decoded) - 1} images a newer one' in caplog.text From 1e1c1417e3261d5cf320454c53183d4a85fdc58b Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Mon, 7 Sep 2026 19:54:55 +0300 Subject: [PATCH 08/12] Name the fake video module once, and state the buffer waiver as a constraint `f'{VENDOR}.video'` is the import name the module object and `sys.modules` must spell alike, so it joins `DEVICE_MODULE` as a constant. The waiver on a buffer of the wrong size carried the rate one station measures and a frame period this driver takes as a parameter. It states what holds for every device instead: the stream goes on, the buffers after it read whole, and the count says how many went. --- positronic/drivers/camera/linux_video.py | 4 ++-- positronic/drivers/camera/tests/conftest.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index 43cc6cac2..252f76e8e 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -92,8 +92,8 @@ def codec_context(codec_name: str) -> av.CodecContext: images = self._images(frame, codec_context) # rules-allow: swallowed-error — a busy bus hands over a buffer that is not a whole frame as - # ordinary traffic, roughly one per camera per minute of capture on the station's four D405; - # the next buffer is a thirtieth of a second away, and the count says how many went + # ordinary traffic; the device keeps streaming, the buffers after it read whole, and the count + # says how many went if images is None: misframed += 1 if misframed == 1: diff --git a/positronic/drivers/camera/tests/conftest.py b/positronic/drivers/camera/tests/conftest.py index b2c8876c8..7cb3a48ef 100644 --- a/positronic/drivers/camera/tests/conftest.py +++ b/positronic/drivers/camera/tests/conftest.py @@ -12,7 +12,8 @@ from enum import Enum VENDOR = 'linuxpy' -DEVICE_MODULE = f'{VENDOR}.video.device' +VIDEO_MODULE = f'{VENDOR}.video' +DEVICE_MODULE = f'{VIDEO_MODULE}.device' if importlib.util.find_spec(VENDOR) is None: # The formats the driver names. The values are the V4L2 four-character codes, as `linuxpy` reports them. @@ -21,10 +22,10 @@ device = types.ModuleType(DEVICE_MODULE) device.__dict__.update(Device=object, PixelFormat=PixelFormat) - video = types.ModuleType(f'{VENDOR}.video') + video = types.ModuleType(VIDEO_MODULE) video.__dict__.update(device=device) package = types.ModuleType(VENDOR) package.__dict__.update(video=video) - sys.modules.update({VENDOR: package, f'{VENDOR}.video': video, DEVICE_MODULE: device}) + sys.modules.update({VENDOR: package, VIDEO_MODULE: video, DEVICE_MODULE: device}) From eb9b422f6c2efeb2ae50e21368ef8984f20a9235 Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Mon, 7 Sep 2026 20:22:05 +0300 Subject: [PATCH 09/12] Take the type errors this branch removed out of the baseline The driver's own frame type ends three errors the baseline grandfathered. --- .basedpyright/baseline.json | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 06a39d376..bd339ba8d 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -3331,30 +3331,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 46, - "endColumn": 50, - "lineCount": 1 - } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 43, - "endColumn": 49, - "lineCount": 1 - } - }, - { - "code": "reportOptionalSubscript", - "range": { - "startColumn": 32, - "endColumn": 38, - "lineCount": 1 - } - }, { "code": "reportReturnType", "range": { @@ -6977,4 +6953,4 @@ } ] } -} +} \ No newline at end of file From 6240b6abae917038ee079b47dc70e242233875a0 Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Tue, 8 Sep 2026 09:04:51 +0300 Subject: [PATCH 10/12] Say that the video driver yields commands, not sleeps `LinuxVideo.run` yields `pimm.Yield()` and declared `Iterator[pimm.Sleep]`; the error the baseline held for that goes with the annotation. `_driven` in the tests took `**kwargs` it could not pass: every constructor argument is already named, so any override raised a duplicate keyword. --- positronic/drivers/camera/linux_video.py | 2 +- positronic/drivers/camera/tests/test_linux_video.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py index 252f76e8e..3d04544b7 100644 --- a/positronic/drivers/camera/linux_video.py +++ b/positronic/drivers/camera/linux_video.py @@ -70,7 +70,7 @@ def _images(self, frame, codec_context) -> list[np.ndarray] | None: raw = self._framed(data, frame, 3) # assume 3 bytes per pixel (RGB/BGR) return None if raw is None else [raw] - def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]: + def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Command]: codec_contexts = {} def codec_context(codec_name: str) -> av.CodecContext: diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py index 225f04916..e625da463 100644 --- a/positronic/drivers/camera/tests/test_linux_video.py +++ b/positronic/drivers/camera/tests/test_linux_video.py @@ -69,11 +69,9 @@ def device(monkeypatch): return FakeDevice -def _driven(device, frames, **kwargs): +def _driven(device, frames): """A driver over ``device`` carrying ``frames``, with its port recorded, run to exhaustion.""" - camera = linux_video.LinuxVideo( - device_path='/dev/null', width=WIDTH, height=HEIGHT, fps=30, pixel_format='YUYV', **kwargs - ) + camera = linux_video.LinuxVideo(device_path='/dev/null', width=WIDTH, height=HEIGHT, fps=30, pixel_format='YUYV') emitted = RecordingEmitter() camera.frame._bind(emitted) device.to_serve = frames From d8c58a45eb21a8020b1855d3b20f18c5a4aae67d Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Tue, 8 Sep 2026 09:05:19 +0300 Subject: [PATCH 11/12] Take the return-type error out of the baseline `LinuxVideo.run` says what it yields, so the entry that grandfathered the wrong annotation has nothing left to hold. --- .basedpyright/baseline.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index bd339ba8d..896f01e0d 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -3330,14 +3330,6 @@ "endColumn": 29, "lineCount": 1 } - }, - { - "code": "reportReturnType", - "range": { - "startColumn": 18, - "endColumn": 30, - "lineCount": 1 - } } ], "./positronic/drivers/camera/luxonis.py": [ From 6efdc484a10f8a1e187d00c53ebb6beecc8b77b4 Mon Sep 17 00:00:00 2001 From: DarksaCY Date: Tue, 8 Sep 2026 09:39:21 +0300 Subject: [PATCH 12/12] Let the fake device record itself when it is opened `FakeDevice.opened` was set in the constructor, so it held the last device built and the assertion that the driver opened one could not fail. --- positronic/drivers/camera/tests/test_linux_video.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py index e625da463..16e730b91 100644 --- a/positronic/drivers/camera/tests/test_linux_video.py +++ b/positronic/drivers/camera/tests/test_linux_video.py @@ -43,10 +43,9 @@ def __init__(self, path: str): self.format = None self.fps = None self.closed = False - FakeDevice.opened = self def open(self) -> None: - pass + FakeDevice.opened = self def set_format(self, buffer, width, height, pixel_format) -> None: self.format = (buffer, width, height, pixel_format)