Is it possible to loop Linux Receiver and UDP Transmitter holoscan operators?

I wrote some python based on the linux_audio_player.py and linux_imx274_player.py examples and tried to get the linux_receiver operator to serves as an input to the udp_transmitter operator for loopback testing without success.

I can send data into the Jetson and I believe the linux_receiver is working, but not the other way.

Any help would be appreciated.

# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import logging
import time

import cuda.bindings.driver as cuda
import holoscan

import hololink as hololink_module


class AudioTransmitApp(holoscan.core.Application):
    def __init__(self, hololink_ip, hololink_channel, cuda_context, camera, frame_limit):
        super().__init__()
        self._hololink_ip = hololink_ip
        self._hololink_channel = hololink_channel
        self._cuda_context = cuda_context
        self._camera = camera
        self._frame_limit = frame_limit

    def compose(self):

        # Create allocator for GPU memory
        allocator = holoscan.resources.UnboundedAllocator(
            self,
            name="allocator",
        )

        # Create UDP receiver operator
        if self._frame_limit:
            self._count = holoscan.conditions.CountCondition(
                self,
                name="count",
                count=self._frame_limit,
            )
            condition = self._count
        else:
            self._ok = holoscan.conditions.BooleanCondition(
                self, name="ok", enable_tick=True
            )
            condition = self._ok

        frame_size = 1024
        frame_context = self._cuda_context

        receiver_operator = hololink_module.operators.LinuxReceiverOperator(
            self,
            condition,
            name="receiver",
            frame_size=frame_size,
            frame_context=frame_context,
            hololink_channel=self._hololink_channel,
            device=self._camera,
        )

        # Create UDP transmitter operator
        udp_transmitter = hololink_module.operators.UdpTransmitterOp(
            self,
            ip=self._hololink_ip,
            port=4791,
            max_buffer_size=32768,
            name="udp_transmitter",
            lossy=False,
        )

        # Connect operators
        self.add_flow(receiver_operator, udp_transmitter, {("output", "input")})


def main():
    parser = argparse.ArgumentParser(description="Audio Transmission Application")

    parser.add_argument(
        "--frame-limit",
        type=int,
        default=None,
        help="Exit after receiving this many frames",
    )

    parser.add_argument(
        "--expander-configuration",
        type=int,
        default=0,
        choices=(0, 1),
        help="I2C Expander configuration",
    )

    parser.add_argument("--hololink", required=True, help="Hololink IP address")

    args = parser.parse_args()

    # Get a handle to the Hololink device
    channel_metadata = hololink_module.Enumerator.find_channel(channel_ip=args.hololink)
    hololink_channel = hololink_module.DataChannel(channel_metadata)

    # Get a handle to the GPU
    (cu_result,) = cuda.cuInit(0)
    assert cu_result == cuda.CUresult.CUDA_SUCCESS
    cu_device_ordinal = 0
    cu_result, cu_device = cuda.cuDeviceGet(cu_device_ordinal)
    assert cu_result == cuda.CUresult.CUDA_SUCCESS
    cu_result, cu_context = cuda.cuDevicePrimaryCtxRetain(cu_device)
    assert cu_result == cuda.CUresult.CUDA_SUCCESS

    # Get a handle to the camera
    camera = hololink_module.sensors.imx274.dual_imx274.Imx274Cam(
        hololink_channel, expander_configuration=args.expander_configuration
    )

    app = AudioTransmitApp(
        hololink_ip=args.hololink,
        hololink_channel=hololink_channel,
        cuda_context=cu_context,
        camera=camera,
        frame_limit=args.frame_limit
    )

    hololink = hololink_channel.hololink()
    hololink.start()
    hololink.reset()

    app.run()

    #hololink.stop()

if __name__ == "__main__":
    main()

Jetson → HSB TX appears to work in this context:
hsb_tx_diag.py.txt (18.0 KB)

Run current v4.1.0 holscan-sensor-bridge container.

cat scripts/holoscan.sh 
#!/usr/bin/bash

xhost +local:docker

docker run --name holoscan -it --rm --net host \
  --runtime=nvidia \
  --privileged \
  --ipc=host --cap-add=CAP_SYS_PTRACE --ulimit memlock=-1 --ulimit stack=67108864 \
  -v $(pwd):/workspace \
  -v /tmp/.X11-unix:/tmp/.X11-unix \
  -e DISPLAY \
  -v "$HOME/.cache:/root/.cache" \
  nvcr.io/nvidia/clara-holoscan/holoscan:v4.1.0-cuda13 bash

# Run this in container

python hsb_tx_diag.py \
  --mode audio-tx \
  --hololink 192.168.0.2 \
  --wav-file ./hsb_test_48k_stereo_s24.wav \
  --chunk-size 192 \
  --readback-tx-registers

# On Thor In second terminal:
sudo tcpdump -i mgbe0_0 -nn -vv udp and host 192.168.0.2 and port 4791

# and here the RX side with dummy device starts:
python hsb_tx_diag.py \
  --mode inspect-rx \
  --hololink 192.168.0.2 \
  --frame-size 1024 \
  --frame-limit 10 \
  --print-every 1 \
  --no-camera-device

If needed, create hsb_test_48k_stereo_s24.wav

python3 - <<'PY'
import math
import wave

sample_rate = 48000
duration_s = 5
frequency = 1000
channels = 2
sample_width = 3  # 24-bit PCM

def int24le(value):
    # clamp signed 24-bit
    value = max(-(1 << 23), min((1 << 23) - 1, value))
    if value < 0:
        value += 1 << 24
    return bytes([
        value & 0xff,
        (value >> 8) & 0xff,
        (value >> 16) & 0xff,
    ])

with wave.open("hsb_test_48k_stereo_s24.wav", "wb") as wav:
    wav.setnchannels(channels)
    wav.setsampwidth(sample_width)
    wav.setframerate(sample_rate)

    frames = bytearray()

    for n in range(sample_rate * duration_s):
        sample = int(0.25 * ((1 << 23) - 1) * math.sin(2 * math.pi * frequency * n / sample_rate))

        # stereo: left + right
        frames += int24le(sample)
        frames += int24le(sample)

    wav.writeframes(frames)

print("wrote hsb_test_48k_stereo_s24.wav")
PY

Thank you! Worked like a charm!

One last question that has popped up, I also saw that there is a “raw-tx” mode. I tried to run it but I kept getting a waterfall of error messages.

I’m trying to get a sense of the actual raw bandwidth for Tx and Rx.

# need these
pip install cupy-cuda13x
pip install -U cuda-python
# This now, finally, works. Completes at "RawUdpSourceOp emitted packet #1000000, size=1024".

python hsb_tx_diag.py --mode raw-tx --hololink 192.168.0.2 --payload-size 1024 --no-camera-device


# For command line args run: 

python hsb_bandwidth_test.py --help

python hsb_tx_diag.py --help

hsb_tx_diag.py.txt (19.1 KB)
hsb_bandwidth_test.py.txt (23.7 KB)

This new version of the “hsb_tx_diag.py” works and I no longer see the waterfall of error messages like above. Thank you for the help!

I have more bandwidth related questions. I’ll start a new forum question about it.

I’m noticing that with the “hsb_tx_diag.py” script in “audio-tx” mode, I’m getting a Jetson->FPGA measured bandwidth of about 300Mbps. I have the MGBE’s in 25G speed mode and they are not aggregated. I was expecting 2,304,000bps since its 48kHz stereo 24-bit. Is this rate of 300Mbps expected? Is there a way to adjust it to make it faster or slower?

I’ve also noticed that for the same Python script in '“raw-tx” mode, there is a gotcha. The UDP port defaults to 4791 which is reserved for ROCEV2 and on the remote end, the receiver thinks it should be getting ROCEV2 packets but in actuality it is a raw UDP packet and missing the ROCEV2 headers. Just something for people to be aware of in the future if they decide to use these scripts that you have so kindly provided.