Skip to content

refactor(camera): use generated CDR values in V4L2 worker - #4656

Closed
TomCC7 wants to merge 5 commits into
cc/feat/cdr-camera-producersfrom
cc/feat/cdr-native-camera-producers
Closed

TomCC7 wants to merge 5 commits into
cc/feat/cdr-camera-producersfrom
cc/feat/cdr-native-camera-producers

Conversation

@TomCC7

@TomCC7 TomCC7 commented Oct 20, 2026

Copy link
Copy Markdown
Member

Linear project: Message Encoding Refactor

V4L2Camera now publishes generated sensor_msgs/msg/CompressedImage values with the Rust SDK CDR codec. The Python wrapper uses the same built-in owner. Device selection, clock correction, retries and JPEG capture stay in the existing worker.

The Nix flake remains self-contained. It fetches one fixed-hash project snapshot, prepares schemas through the existing Python build core and published dimos-cdrgen==0.1.1, and assembles the SDK, generated owner and local worker at their Cargo-relative paths. It does not reach outside the module directory. No upstream .msg files or generated classes are checked in. Macro validation now accepts canonical slash-separated names while retaining legacy dotted names and payload mismatch errors.

Dependent draft after #4655. RealSense/ZED, native CDR TF, downstream perception and default-transport cutover remain later slices. No worker binary, V4L2 device, Jetson driver or robot was started. No new generator release or package registry publication.

Validation

At 5385ae1df08b5ae8091cdc2f4aee9002cf73acd6:

  • 20 macro tests passed, including canonical names and mismatched payload rejection.
  • The exact offline generation recipe produced 77 Rust source/schema files byte-identical to ordinary main-package generation.
  • A separate source assembly used the actual pinned project archive, generated owner and current worker. Six Rust library tests passed with --locked --offline, including software JPEG and CDR. Clippy passed with warnings denied.
  • An actual Rust-emitted 52-byte CompressedImage fixture decoded as the installed Python owner, preserving Header timestamp/frame, format and payload. Python re-encoding produced identical bytes. Its payload is a minimal codec fixture; separate software-JPEG tests check real JPEG encoding/decoding.
  • Existing native input-hash isolation accepted exactly the module directory. That gate was not changed.
  • Strict Python wrapper typing passed. The normal main wheel built; its installed wrapper resolves the exact built-in owner, and pip check passed.
  • Available hooks passed, including Cargo format. Local native Nix lint was skipped because no Nix executable is installed. The Nix derivation, ARM/Jetson and physical capture have not been validated locally. CI was not polled; this is not an all-green CI claim.

The Nix project source is pinned to 3d68b9a7966fd7e447642bb72f3dc8ee8579f865 (the first preparation commit in this PR), archive SHA256 288b8b2bc9b00a3ba8cc58efc6340001b9eca1b9ba9eff01e94ebd487da7d304. The published generator wheel is separately hash-pinned. Generation runs offline after those inputs are fetched.

Try it without a camera

From this PR's checkout and an environment containing Nix:

cd dimos/hardware/sensors/camera/v4l2/rust
nix build path:.

For normal repository development with Rust/C toolchains, prepare messages through the ordinary editable Python install, then run only library tests:

uv pip install -e .
cargo test --manifest-path dimos/hardware/sensors/camera/v4l2/rust/Cargo.toml \
  --locked --lib --jobs 2

The embedded walkthrough explains the artifacts and prerequisites. The equivalent isolated Cargo source assembly was executed locally; the Nix command above remains an unverified gate. Do not launch the camera binary as an offline check.

File change summary

Relative to #4655 at 7e2452385ddbf60b19cf9c979e17eefb0109dcf7: nine files, 196 additions and 17 deletions; binary lockfile excluded from line counts.

Category Files Added Deleted
Shared source generation and macro validation 2 77 1
Native/Python producer and codec tests 3 49 12
Isolated Nix assembly 1 25 2
Documentation and CI 2 45 2
Cargo dependency lock 1 binary binary

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go2 replay realtime (arm64)

Details
Benchmark suite Current: ac112ce Previous: 17fdb6b Ratio
peak memory 1631.391 MB 1589.477 MB 1.03
peak threads 358 threads 350 threads 1.02
network (transport) 2721.643 MB 2730.079 MB 1.00
disk write 1.914 MB 1.707 MB 1.12
instructions 122.313 G 115.497 G 1.06

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

❌ 25 Tests Failed:

Tests completed Failed Passed Skipped
6604 25 6579 53
View the top 3 failed test(s) by shortest run time
dimos.hardware.sensors.camera.test_webcam::test_capture_converts_bgr_and_crops_before_cdr_encoding[left-0]
Stack Traces | 0.003s run time
side = 'left', start = 0
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f620dc83ca0>

    @pytest.mark.parametrize("side,start", [("left", 0), ("right", 2)])
    def test_capture_converts_bgr_and_crops_before_cdr_encoding(side, start, mocker):
>       camera = Webcam(stereo_slice=side, frame_id_prefix="robot")

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f620dc83ca0>
side       = 'left'
start      = 0

.../sensors/camera/test_webcam.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'frame_id_prefix': 'robot', 'stereo_slice': 'left'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc83b20>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc83b20>
kwargs = {'frame_id_prefix': 'robot', 'stereo_slice': 'left'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'frame_id_prefix': 'robot', 'stereo_slice': 'left'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc83b20>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam::test_webcam_stereo_slice_halves_the_nominal_pinhole_width
Stack Traces | 0.003s run time
def test_webcam_stereo_slice_halves_the_nominal_pinhole_width() -> None:
>       info = Webcam(width=1280, height=720, stereo_slice="left").camera_info


.../sensors/camera/test_webcam.py:61: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'height': 720, 'stereo_slice': 'left', 'width': 1280}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dd33160>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dd33160>
kwargs = {'height': 720, 'stereo_slice': 'left', 'width': 1280}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'height': 720, 'stereo_slice': 'left', 'width': 1280}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dd33160>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[darwin-/dev/video0-/dev/video0-0]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d304a4700>
request = <SubRequest 'webcam' for <Function test_capture_backend[.../video0-/dev/video0-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d304a4700>
request    = <SubRequest 'webcam' for <Function test_capture_backend[.../video0-/dev/video0-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': '/dev/video0'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d304a4160>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d304a4160>
kwargs = {'camera_index': '/dev/video0'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': '/dev/video0'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d304a4160>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux- -1 --1-0]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d3034fb50>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux- -1 --1-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d3034fb50>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux- -1 --1-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': ' -1 '}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3034e2f0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3034e2f0>
kwargs = {'camera_index': ' -1 '}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': ' -1 '}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3034e2f0>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux- 2 -2-0]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d30492f50>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux- 2 -2-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d30492f50>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux- 2 -2-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': ' 2 '}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30491930>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30491930>
kwargs = {'camera_index': ' 2 '}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': ' 2 '}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30491930>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux-+2-2-0]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d30323df0>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux-+2-2-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d30323df0>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux-+2-2-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': '+2'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30321300>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30321300>
kwargs = {'camera_index': '+2'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': '+2'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30321300>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux--1--1-0]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d30320d90>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux--1--1-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d30320d90>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux--1--1-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': '-1'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30320d00>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30320d00>
kwargs = {'camera_index': '-1'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': '-1'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30320d00>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux--2--2-0]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2b1d6ac7c0>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux--2--2-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2b1d6ac7c0>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux--2--2-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': '-2'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2b1d6ad870>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2b1d6ad870>
kwargs = {'camera_index': '-2'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': '-2'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2b1d6ad870>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux-2-2-0_1]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d3047b6d0>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux-2-2-0_1]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d3047b6d0>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux-2-2-0_1]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': '2'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3047b610>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3047b610>
kwargs = {'camera_index': '2'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': '2'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3047b610>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux-rtsp://camera/stream-rtsp://camera/stream-0]
Stack Traces | 0.003s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d30490130>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux-rtsp:.../camera/stream-rtsp:.../camera/stream-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d30490130>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux-rtsp:.../camera/stream-rtsp:.../camera/stream-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': 'rtsp:.../camera/stream'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30490c70>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30490c70>
kwargs = {'camera_index': 'rtsp:.../camera/stream'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': 'rtsp:.../camera/stream'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d30490c70>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam::test_capture_converts_bgr_and_crops_before_cdr_encoding[right-2]
Stack Traces | 0.004s run time
side = 'right', start = 2
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f620dc5d330>

    @pytest.mark.parametrize("side,start", [("left", 0), ("right", 2)])
    def test_capture_converts_bgr_and_crops_before_cdr_encoding(side, start, mocker):
>       camera = Webcam(stereo_slice=side, frame_id_prefix="robot")

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f620dc5d330>
side       = 'right'
start      = 2

.../sensors/camera/test_webcam.py:67: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'frame_id_prefix': 'robot', 'stereo_slice': 'right'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc5ca90>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc5ca90>
kwargs = {'frame_id_prefix': 'robot', 'stereo_slice': 'right'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'frame_id_prefix': 'robot', 'stereo_slice': 'right'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc5ca90>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[darwin-0-0-0]
Stack Traces | 0.004s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d30309960>
request = <SubRequest 'webcam' for <Function test_capture_backend[darwin-0-0-0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d30309960>
request    = <SubRequest 'webcam' for <Function test_capture_backend[darwin-0-0-0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': 0}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3030b6d0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3030b6d0>
kwargs = {'camera_index': 0}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': 0}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3030b6d0>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux-2-2-0_0]
Stack Traces | 0.004s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2b1d6adba0>
request = <SubRequest 'webcam' for <Function test_capture_backend[linux-2-2-0_0]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2b1d6adba0>
request    = <SubRequest 'webcam' for <Function test_capture_backend[linux-2-2-0_0]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': 2}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2b1d6ade70>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2b1d6ade70>
kwargs = {'camera_index': 2}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': 2}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2b1d6ade70>

.../protocol/service/spec.py:47: PydanticUserError
dimos.web.relay_bridge.test_relay_bridge_module::test_relay_key_falls_back_to_global_config
Stack Traces | 0.004s run time
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff62dc71ef90>

    def test_relay_key_falls_back_to_global_config(monkeypatch) -> None:
        # RELAY_KEY (env or .env) lands in GlobalConfig; the module reads it when
        # its own field is unset. The autouse fixture restores global_config.
        client = FakeClient()
    
        async def fake_connect(url: str, role: str, **kwargs: Any) -> FakeClient:
            return client
    
        patch_relay(monkeypatch, fake_connect)
        global_config.update(relay_key="global-key-0123456789abcdef")
        module = RelayBridgeModule(
            relay_url="http://127.0.0.1:7780", open_browser=False, robot_id="unit-bot"
        )
        try:
>           module.start()

client     = <dimos.web.relay_bridge.module_test_support.FakeClient object at 0xff62dc71e000>
fake_connect = <function test_relay_key_falls_back_to_global_config.<locals>.fake_connect at 0xff62dd978720>
module     = <dimos.web.relay_bridge.relay_bridge_module.RelayBridgeModule object at 0xff62dc71e600>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff62dc71ef90>

.../web/relay_bridge/test_relay_bridge_module.py:1047: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/core/module.py:211: in start
    self._start_main()
        self       = <dimos.web.relay_bridge.relay_bridge_module.RelayBridgeModule object at 0xff62dc71e600>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.web.relay_bridge.relay_bridge_module.RelayBridgeModule object at 0xff62dc71e600>

    def _start_main(self) -> None:
        """
        If the subclass defines `async def main(self)` as an async generator
        with exactly one `yield`, run everything before the `yield` as part of
        start().
        """
        main_fn = getattr(type(self), "main", None)
        if main_fn is None:
            return
        if not inspect.isasyncgenfunction(main_fn):
            raise TypeError(
                f"{type(self).__name__}.main must be an `async def` with exactly "
                "one `yield` (an async generator function)"
            )
        loop = self._loop
        if loop is None or not loop.is_running():
>           raise RuntimeError(f"{type(self).__name__}._loop is not running")
E           RuntimeError: RelayBridgeModule._loop is not running

loop       = <_UnixSelectorEventLoop running=False closed=False debug=False>
main_fn    = <function RelayBridgeModule.main at 0xff639ecb9580>
self       = <dimos.web.relay_bridge.relay_bridge_module.RelayBridgeModule object at 0xff62dc71e600>

dimos/core/module.py:655: RuntimeError
dimos.hardware.sensors.camera.test_webcam::test_webcam_with_size_but_no_focal_length_gets_a_nominal_pinhole
Stack Traces | 0.005s run time
def test_webcam_with_size_but_no_focal_length_gets_a_nominal_pinhole() -> None:
>       info = Webcam(
            camera_info=CameraInfo(
                width=1280,
                height=720,
                header=Header(stamp=Time(sec=0, nanosec=0), frame_id=""),
                distortion_model="",
                d=np.array([], dtype=np.float64),
                k=np.zeros(9, dtype=np.float64),
                r=np.zeros(9, dtype=np.float64),
                p=np.zeros(12, dtype=np.float64),
                binning_x=0,
                binning_y=0,
                roi=RegionOfInterest(x_offset=0, y_offset=0, height=0, width=0, do_rectify=False),
            )
        ).camera_info


.../sensors/camera/test_webcam.py:42: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_info': sensor_msgs__msg__CameraInfo(header=std_msgs__msg__Header(stamp=builtin_interfaces__msg__Time(sec=0, n... width=0, do_rectify=False, __msgtype__='sensor_msgs/msg/RegionOfInterest'), __msgtype__='sensor_msgs/msg/CameraInfo')}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc52920>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc52920>
kwargs = {'camera_info': sensor_msgs__msg__CameraInfo(header=std_msgs__msg__Header(stamp=builtin_interfaces__msg__Time(sec=0, n... width=0, do_rectify=False, __msgtype__='sensor_msgs/msg/RegionOfInterest'), __msgtype__='sensor_msgs/msg/CameraInfo')}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_info': sensor_msgs__msg__CameraInfo(header=std_msgs__msg__Header(stamp=builtin_interfaces__msg__Time(sec=0, n... width=0, do_rectify=False, __msgtype__='sensor_msgs/msg/RegionOfInterest'), __msgtype__='sensor_msgs/msg/CameraInfo')}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc52920>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam::test_webcam_keeps_configured_intrinsics
Stack Traces | 0.006s run time
def test_webcam_keeps_configured_intrinsics() -> None:
        configured = camera_info_from_fov(
            90.0, 640, 480, header=Header(stamp=Time(sec=0, nanosec=0), frame_id="")
        )
>       assert Webcam(camera_info=configured).camera_info is configured

configured = sensor_msgs__msg__CameraInfo(header=std_msgs__msg__Header(stamp=builtin_interfaces__msg__Time(sec=0, nanosec=0, __msgt..., width=0, do_rectify=False, __msgtype__='sensor_msgs/msg/RegionOfInterest'), __msgtype__='sensor_msgs/msg/CameraInfo')

.../sensors/camera/test_webcam.py:38: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_info': sensor_msgs__msg__CameraInfo(header=std_msgs__msg__Header(stamp=builtin_interfaces__msg__Time(sec=0, n... width=0, do_rectify=False, __msgtype__='sensor_msgs/msg/RegionOfInterest'), __msgtype__='sensor_msgs/msg/CameraInfo')}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dd33a90>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dd33a90>
kwargs = {'camera_info': sensor_msgs__msg__CameraInfo(header=std_msgs__msg__Header(stamp=builtin_interfaces__msg__Time(sec=0, n... width=0, do_rectify=False, __msgtype__='sensor_msgs/msg/RegionOfInterest'), __msgtype__='sensor_msgs/msg/CameraInfo')}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_info': sensor_msgs__msg__CameraInfo(header=std_msgs__msg__Header(stamp=builtin_interfaces__msg__Time(sec=0, n... width=0, do_rectify=False, __msgtype__='sensor_msgs/msg/RegionOfInterest'), __msgtype__='sensor_msgs/msg/CameraInfo')}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dd33a90>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam::test_webcam_without_intrinsics_reports_a_nominal_pinhole
Stack Traces | 0.006s run time
def test_webcam_without_intrinsics_reports_a_nominal_pinhole() -> None:
>       info = Webcam(width=1280, height=720).camera_info


.../sensors/camera/test_webcam.py:28: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'height': 720, 'width': 1280}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc51db0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc51db0>
kwargs = {'height': 720, 'width': 1280}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'height': 720, 'width': 1280}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f620dc51db0>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux-/dev/video0-/dev/video0-200]
Stack Traces | 0.006s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d301ba2c0>
request = <SubRequest 'webcam' for <Function test_capture_backend[.../video0-/dev/video0-200]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d301ba2c0>
request    = <SubRequest 'webcam' for <Function test_capture_backend[.../video0-/dev/video0-200]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': '/dev/video0'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d301ba230>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d301ba230>
kwargs = {'camera_index': '/dev/video0'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': '/dev/video0'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d301ba230>

.../protocol/service/spec.py:47: PydanticUserError
dimos.hardware.sensors.camera.test_webcam_backend::test_capture_backend[linux-/dev/v4l/by-id/usb-camera-/dev/v4l/by-id/usb-camera-200]
Stack Traces | 0.007s run time
mocker = <pytest_mock.plugin.MockerFixture object at 0x7f2d3003d000>
request = <SubRequest 'webcam' for <Function test_capture_backend[.../v4l/by-id/usb-camera-200]>>

    @pytest.fixture
    def webcam(mocker, request):
>       camera = Webcam(camera_index=request.param)

mocker     = <pytest_mock.plugin.MockerFixture object at 0x7f2d3003d000>
request    = <SubRequest 'webcam' for <Function test_capture_backend[.../v4l/by-id/usb-camera-200]>>

.../sensors/camera/test_webcam_backend.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': '.../v4l/by-id/usb-camera'}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3003ce20>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3003ce20>
kwargs = {'camera_index': '.../v4l/by-id/usb-camera'}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': '.../v4l/by-id/usb-camera'}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d3003ce20>

.../protocol/service/spec.py:47: PydanticUserError
dimos.perception.fiducial.blueprints.test_desk_marker_tf::test_create_desk_webcam_loads_camera_info_yaml
Stack Traces | 0.012s run time
tmp_path = PosixPath('.../pytest-0/popen-gw2/test_create_desk_webcam_loads_0')

    def test_create_desk_webcam_loads_camera_info_yaml(tmp_path: Path) -> None:
        camera_info_yaml = tmp_path / "camera_info.yaml"
        camera_info_yaml.write_text(
            """
    image_width: 1920
    image_height: 1080
    camera_name: macbook_pro_14_2025_center_stage
    distortion_model: plumb_bob
    camera_matrix:
      rows: 3
      cols: 3
      data: [2236.0, 0.0, 990.0, 0.0, 2378.0, 568.0, 0.0, 0.0, 1.0]
    distortion_coefficients:
      rows: 1
      cols: 5
      data: [1.7, -24.5, -0.03, -0.1, 212.1]
    rectification_matrix:
      rows: 3
      cols: 3
      data: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
    projection_matrix:
      rows: 3
      cols: 4
      data: [2236.0, 0.0, 990.0, 0.0, 0.0, 2378.0, 568.0, 0.0, 0.0, 0.0, 1.0, 0.0]
    """.lstrip()
        )
    
>       camera = create_desk_webcam(camera_info_yaml, camera_index=1, fps=7.5)

camera_info_yaml = PosixPath('.../pytest-0/popen-gw2/test_create_desk_webcam_loads_0/camera_info.yaml')
tmp_path   = PosixPath('.../pytest-0/popen-gw2/test_create_desk_webcam_loads_0')

.../fiducial/blueprints/test_desk_marker_tf.py:83: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../fiducial/blueprints/desk_marker_tf.py:52: in create_desk_webcam
    return Webcam(
        camera_index = 1
        camera_info = CameraInfo(height=1080, width=1920, distortion_model='plumb_bob', frame_id='camera_optical', ts=1792466885.3116896)
        camera_info_yaml = PosixPath('.../pytest-0/popen-gw2/test_create_desk_webcam_loads_0/camera_info.yaml')
        fps        = 7.5
.../sensors/camera/webcam.py:76: in __init__
    super().__init__(*args, **kwargs)
        __class__  = <class 'dimos.hardware.sensors.camera.webcam.Webcam'>
        args       = ()
        kwargs     = {'camera_index': 1, 'camera_info': CameraInfo(height=1080, width=1920, distortion_model='plumb_bob', frame_id='camera_optical', ts=1792466885.3116896), 'fps': 7.5, 'height': 1080, ...}
        self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d2fc2f2e0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d2fc2f2e0>
kwargs = {'camera_index': 1, 'camera_info': CameraInfo(height=1080, width=1920, distortion_model='plumb_bob', frame_id='camera_optical', ts=1792466885.3116896), 'fps': 7.5, 'height': 1080, ...}
config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>

    def __init__(self, **kwargs: Any) -> None:
        config_type = get_type_hints(type(self))["config"]
>       self.config = config_type(**kwargs)
E       pydantic.errors.PydanticUserError: `WebcamConfig` is not fully defined; you should define `np`, then call `WebcamConfig.model_rebuild()`.
E       
E       For further information visit https://errors.pydantic.dev/2.12/u/class-not-fully-defined

config_type = <class 'dimos.hardware.sensors.camera.webcam.WebcamConfig'>
kwargs     = {'camera_index': 1, 'camera_info': CameraInfo(height=1080, width=1920, distortion_model='plumb_bob', frame_id='camera_optical', ts=1792466885.3116896), 'fps': 7.5, 'height': 1080, ...}
self       = <dimos.hardware.sensors.camera.webcam.Webcam object at 0x7f2d2fc2f2e0>

.../protocol/service/spec.py:47: PydanticUserError
dimos.cli.bake.test_discovery::test_every_baked_port_lands_on_the_key_its_python_wrapper_subscribes_to
Stack Traces | 0.035s run time
def test_every_baked_port_lands_on_the_key_its_python_wrapper_subscribes_to() -> None:
        """A manifest naming the rust payload rather than the dimos message goes dark."""
        for module in discover_modules().values():
            ports = wrapper_ports(module.python_ref)
            for port, msg_type in module.ports.items():
                assert port in ports, f"{module.id}.{port} is not a port of {module.python_ref}"
>               subscribed = ZenohTopic(f"dimos/{port}", ports[port]).key_expr

module     = RegisteredModule(id='v4l2_camera', crate_dir=PosixPath('.../sensors/camera/v4....sensors.camera.v4l2.module:V4L2Camera', threads=1, inputs={}, outputs={'jpeg_out': 'sensor_msgs/msg/CompressedImage'})
msg_type   = 'sensor_msgs/msg/CompressedImage'
port       = 'jpeg_out'
ports      = {'jpeg_out': <class 'rosbags.usertypes.sensor_msgs__msg__CompressedImage'>}
subscribed = 'dimos/infrared_right_camera_info/sensor_msgs.CameraInfo'

.../cli/bake/test_discovery.py:164: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = Topic(topic='dimos/jpeg_out', lcm_type=<class 'rosbags.usertypes.sensor_msgs__msg__CompressedImage'>, qos=None)

    @property
    def key_expr(self) -> str:
        """The Zenoh key expression for this topic, embedding lcm_type after a '/'.
    
        Examples:
            Topic("dimos/cmd_vel", Twist) -> "dimos/cmd_vel/geometry_msgs.Twist"
            Topic("dimos/data")           -> "dimos/data"
        """
        if self.lcm_type is not None:
>           return f"{self.pattern}/{self.lcm_type.msg_name}"
E           AttributeError: type object 'sensor_msgs__msg__CompressedImage' has no attribute 'msg_name'

self       = Topic(topic='dimos/jpeg_out', lcm_type=<class 'rosbags.usertypes.sensor_msgs__msg__CompressedImage'>, qos=None)

.../pubsub/impl/zenohpubsub.py:95: AttributeError
dimos.codebase_checks.test_docs_branding::test_docs_use_current_branding
Stack Traces | 0.061s run time
def test_docs_use_current_branding() -> None:
        """Fail if any file under docs/ spells the brand "DimOS" instead of "dimOS"."""
        hits = find_old_branding()
        if hits:
            listing = "\n".join(
                f"  - {p.relative_to(DIMOS_PROJECT_ROOT)}:{lineno}: {line.strip()}"
                for p, lineno, line in hits
            )
>           raise AssertionError(f'Found "DimOS" in docs/:\n{listing}\n\nThe brand is spelled "dimOS".')
E           AssertionError: Found "DimOS" in docs/:
E             - docs/usage/visualization.md:179: DimOS environment:
E           
E           The brand is spelled "dimOS".

hits       = [(PosixPath('.../docs/usage/visualization.md'), 179, 'DimOS environment:')]
listing    = '  - docs/usage/visualization.md:179: DimOS environment:'

dimos/codebase_checks/test_docs_branding.py:43: AssertionError
dimos.codebase_checks.test_inline_heavy_imports::test_heavy_imports_are_inline
Stack Traces | 2.91s run time
def test_heavy_imports_are_inline() -> None:
        """Fail if any file imports cv2/open3d/rerun at module level."""
        hits = find_eager_heavy_imports()
        if hits:
            listing = "\n".join(
                f"  - dimos/{f}:{line}: `{module}`"
                for f, lines in sorted(hits.items())
                for line, module in lines
            )
>           raise AssertionError(
                f"Found module-level import(s) of {'/'.join(HEAVY_MODULES)}:\n{listing}\n\n"
                "These libraries load large native extensions into every process that "
                "transitively imports the module. Import them inside the function or "
                "method that uses them; imports needed only for type annotations go "
                "under `if TYPE_CHECKING:`."
            )
E           AssertionError: Found module-level import(s) of cv2/open3d/rerun:
E             - dimos/msgs/image.py:22: `cv2`
E             - dimos/msgs/pointcloud.py:24: `open3d`
E           
E           These libraries load large native extensions into every process that transitively imports the module. Import them inside the function or method that uses them; imports needed only for type annotations go under `if TYPE_CHECKING:`.

hits       = {'msgs/image.py': [(22, 'cv2')], 'msgs/pointcloud.py': [(24, 'open3d')]}
listing    = '  - dimos/msgs/image.py:22: `cv2`\n  - dimos/msgs/pointcloud.py:24: `open3d`'

dimos/codebase_checks/test_inline_heavy_imports.py:92: AssertionError
dimos.cli.test_cli_startup::test_go2_blueprint_import_does_not_pull_heavy_deps
Stack Traces | 4.01s run time
def test_go2_blueprint_import_does_not_pull_heavy_deps() -> None:
        # Importing the blueprint builds LCM handles. memq keeps them off the network.
        env = {**os.environ, "LCM_DEFAULT_URL": "memq://"}
        leaked = _leaked_modules(
            "import dimos.robot.unitree.go2.blueprints.smart.unitree_go2",
            BLUEPRINT_MUST_NOT_IMPORT,
            env=env,
        )
>       assert leaked == []
E       AssertionError: assert ['open3d', 'cv2'] == []
E         
E         Left contains 2 more items, first extra item: #x1B[0m#x1B[33m'#x1B[39;49;00m#x1B[33mopen3d#x1B[39;49;00m#x1B[33m'#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E         
E         Full diff:
E         #x1B[0m#x1B[91m- []#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E         #x1B[92m+ [#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E         #x1B[92m+     'open3d',#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E         #x1B[92m+     'cv2',#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
E         #x1B[92m+ ]#x1B[39;49;00m#x1B[90m#x1B[39;49;00m

env        = {'ACCEPT_EULA': 'Y', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4ODI2YjE3LTZhMzAtNWY5Yi1iMTY5LT...-version=2.0', 'ACTIONS_ORCHESTRATION_ID': '3536d3a6-0867-8ede-b0f4-d9f6ebfed144.tests._3_10_ubuntu-latest_false', ...}
leaked     = ['open3d', 'cv2']

dimos/cli/test_cli_startup.py:135: AssertionError
dimos.hardware.sensors.camera.test_module::test_disabled_mount_publishes_calibration_without_tf
Stack Traces | 5.01s run time
request = <SubRequest 'monitor_threads' for <Function test_disabled_mount_publishes_calibration_without_tf>>

    @pytest.fixture(autouse=True)
    def monitor_threads(request):
        # Capture threads before test runs
        test_name = request.node.nodeid
        with _seen_threads_lock:
            _before_test_threads[test_name] = {
                t.ident for t in threading.enumerate() if t.ident is not None
            }
    
        yield
    
        with _seen_threads_lock:
            before = _before_test_threads.get(test_name, set())
    
        # Threads intentionally left running for the whole process and cleaned up on
        # exit, so they don't count as per-test leaks.
        expected_persistent_thread_prefixes = [
            "Dask-Offload",
            # HuggingFace safetensors conversion thread - no user cleanup API
            # https://github..../transformers/issues/29513
            "Thread-auto_conversion",
        ]
        # Zenoh callback threads belong to a session in the process-wide pool, which
        # outlives the test that first opened it on purpose -- sharing one session is
        # the point of the pool, and closing it per test would cut module-scoped
        # fixtures off from their transports mid-module. They go at interpreter exit.
        # The name is "Thread-<n> (pyo3-closure)", so this cannot be a prefix match.
        expected_persistent_thread_infix = "(pyo3-closure)"
    
        def live_new_threads():
            # Threads created during this test that are still running. A thread that
            # has already stopped is not a leak -- it's done, just not yet reaped
            # from threading's registry -- so we key on is_alive(), not presence.
            result = []
            for t in threading.enumerate():
                if t.ident is None or t.ident in before or t.name == "MainThread":
                    continue
                if any(t.name.startswith(prefix) for prefix in expected_persistent_thread_prefixes):
                    continue
                if expected_persistent_thread_infix in t.name:
                    continue
                if t.is_alive():
                    result.append(t)
            return result
    
        # Some C extensions tear their callback threads down asynchronously, so a
        # thread can stay alive briefly after the test cleaned up its owner (notably
        # zenoh's pyo3-closure threads, freed shortly after the session is closed).
        # A single snapshot races that teardown and flags a thread that is about to
        # exit. Give new threads a grace period to drain; only ones that stay alive
        # are real leaks (a genuinely leaked thread never exits).
        deadline = time.monotonic() + 5.0
        leaked = live_new_threads()
        while leaked and time.monotonic() < deadline:
            time.sleep(0.02)
            leaked = live_new_threads()
    
        if not leaked:
            return
    
        with _seen_threads_lock:
            # Report each leaked thread only once across the session.
            truly_new = [t for t in leaked if t.ident not in _seen_threads]
            for t in leaked:
                _seen_threads.add(t.ident)
    
        if not truly_new:
            return
    
        thread_names = [t.name for t in truly_new]
    
>       pytest.fail(
            f"Non-closed threads created during this test. Thread names: {thread_names}. "
            "Please look at the first test that fails and fix that."
        )
E       Failed: Non-closed threads created during this test. Thread names: ['Thread-3629 (run_forever)']. Please look at the first test that fails and fix that.

before     = {140055997773504, 140057608451776, 140068431218496}
deadline   = 421.024821595
expected_persistent_thread_infix = '(pyo3-closure)'
expected_persistent_thread_prefixes = ['Dask-Offload', 'Thread-auto_conversion']
leaked     = [<Thread(Thread-3629 (run_forever), started daemon 140058982086336)>]
live_new_threads = <function monitor_threads.<locals>.live_new_threads at 0x7f620cf20670>
request    = <SubRequest 'monitor_threads' for <Function test_disabled_mount_publishes_calibration_without_tf>>
t          = <Thread(Thread-3629 (run_forever), started daemon 140058982086336)>
test_name  = '.../sensors/camera/test_module.py::test_disabled_mount_publishes_calibration_without_tf'
thread_names = ['Thread-3629 (run_forever)']
truly_new  = [<Thread(Thread-3629 (run_forever), started daemon 140058982086336)>]

dimos/conftest.py:432: Failed
dimos.hardware.sensors.camera.test_module::test_metadata_preserves_calibration_and_does_not_mutate_mount_template
Stack Traces | 5.02s run time
request = <SubRequest 'monitor_threads' for <Function test_metadata_preserves_calibration_and_does_not_mutate_mount_template>>

    @pytest.fixture(autouse=True)
    def monitor_threads(request):
        # Capture threads before test runs
        test_name = request.node.nodeid
        with _seen_threads_lock:
            _before_test_threads[test_name] = {
                t.ident for t in threading.enumerate() if t.ident is not None
            }
    
        yield
    
        with _seen_threads_lock:
            before = _before_test_threads.get(test_name, set())
    
        # Threads intentionally left running for the whole process and cleaned up on
        # exit, so they don't count as per-test leaks.
        expected_persistent_thread_prefixes = [
            "Dask-Offload",
            # HuggingFace safetensors conversion thread - no user cleanup API
            # https://github..../transformers/issues/29513
            "Thread-auto_conversion",
        ]
        # Zenoh callback threads belong to a session in the process-wide pool, which
        # outlives the test that first opened it on purpose -- sharing one session is
        # the point of the pool, and closing it per test would cut module-scoped
        # fixtures off from their transports mid-module. They go at interpreter exit.
        # The name is "Thread-<n> (pyo3-closure)", so this cannot be a prefix match.
        expected_persistent_thread_infix = "(pyo3-closure)"
    
        def live_new_threads():
            # Threads created during this test that are still running. A thread that
            # has already stopped is not a leak -- it's done, just not yet reaped
            # from threading's registry -- so we key on is_alive(), not presence.
            result = []
            for t in threading.enumerate():
                if t.ident is None or t.ident in before or t.name == "MainThread":
                    continue
                if any(t.name.startswith(prefix) for prefix in expected_persistent_thread_prefixes):
                    continue
                if expected_persistent_thread_infix in t.name:
                    continue
                if t.is_alive():
                    result.append(t)
            return result
    
        # Some C extensions tear their callback threads down asynchronously, so a
        # thread can stay alive briefly after the test cleaned up its owner (notably
        # zenoh's pyo3-closure threads, freed shortly after the session is closed).
        # A single snapshot races that teardown and flags a thread that is about to
        # exit. Give new threads a grace period to drain; only ones that stay alive
        # are real leaks (a genuinely leaked thread never exits).
        deadline = time.monotonic() + 5.0
        leaked = live_new_threads()
        while leaked and time.monotonic() < deadline:
            time.sleep(0.02)
            leaked = live_new_threads()
    
        if not leaked:
            return
    
        with _seen_threads_lock:
            # Report each leaked thread only once across the session.
            truly_new = [t for t in leaked if t.ident not in _seen_threads]
            for t in leaked:
                _seen_threads.add(t.ident)
    
        if not truly_new:
            return
    
        thread_names = [t.name for t in truly_new]
    
>       pytest.fail(
            f"Non-closed threads created during this test. Thread names: {thread_names}. "
            "Please look at the first test that fails and fix that."
        )
E       Failed: Non-closed threads created during this test. Thread names: ['Thread-3627 (run_forever)']. Please look at the first test that fails and fix that.

before     = {140055997773504, 140068431218496}
deadline   = 415.966335658
expected_persistent_thread_infix = '(pyo3-closure)'
expected_persistent_thread_prefixes = ['Dask-Offload', 'Thread-auto_conversion']
leaked     = [<Thread(Thread-3627 (run_forever), started daemon 140057608451776)>]
live_new_threads = <function monitor_threads.<locals>.live_new_threads at 0x7f620cf20e50>
request    = <SubRequest 'monitor_threads' for <Function test_metadata_preserves_calibration_and_does_not_mutate_mount_template>>
t          = <Thread(Thread-3627 (run_forever), started daemon 140057608451776)>
test_name  = '.../sensors/camera/test_module.py::test_metadata_preserves_calibration_and_does_not_mutate_mount_template'
thread_names = ['Thread-3627 (run_forever)']
truly_new  = [<Thread(Thread-3627 (run_forever), started daemon 140057608451776)>]

dimos/conftest.py:432: Failed

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@TomCC7 TomCC7 closed this Oct 20, 2026
@TomCC7
TomCC7 deleted the cc/feat/cdr-native-camera-producers branch October 20, 2026 18:10

This branch was successfully deployed

1 active deployment
cachix — ac112ced Deployed Oct 20, 2026 by TomCC7 via cachix-build-macos #13370
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant