Пример #1
0
async def _unittest_can_pythoncan_socketcan() -> None:
    asyncio.get_running_loop().slow_callback_duration = 5.0

    media_a = PythonCANMedia("socketcan:vcan2", 0, 8)
    media_b = PythonCANMedia("socketcan:vcan2", 0, 64)

    rx_a: typing.List[typing.Tuple[Timestamp, Envelope]] = []
    rx_b: typing.List[typing.Tuple[Timestamp, Envelope]] = []

    def on_rx_a(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None:
        nonlocal rx_a
        rx_a += list(frames)

    def on_rx_b(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None:
        nonlocal rx_b
        rx_b += list(frames)

    media_a.start(on_rx_a, no_automatic_retransmission=False)
    media_b.start(on_rx_b, no_automatic_retransmission=False)

    ts_begin = Timestamp.now()
    await media_a.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 0xBADC0FE, bytearray(b"123")), loopback=True),
            Envelope(DataFrame(FrameFormat.EXTENDED, 0x12345678, bytearray(b"456")), loopback=False),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    await asyncio.sleep(1.0)
    ts_end = Timestamp.now()

    assert len(rx_b) == 2
    assert ts_begin.monotonic_ns <= rx_b[0][0].monotonic_ns <= ts_end.monotonic_ns
    assert ts_begin.monotonic_ns <= rx_b[1][0].monotonic_ns <= ts_end.monotonic_ns
    assert ts_begin.system_ns <= rx_b[0][0].system_ns <= ts_end.system_ns
    assert ts_begin.system_ns <= rx_b[1][0].system_ns <= ts_end.system_ns
    assert not rx_b[0][1].loopback
    assert not rx_b[1][1].loopback
    assert rx_b[0][1].frame.identifier == 0xBADC0FE
    assert rx_b[1][1].frame.identifier == 0x12345678
    assert rx_b[0][1].frame.data == b"123"
    assert rx_b[1][1].frame.data == b"456"

    assert len(rx_a) == 1
    assert ts_begin.monotonic_ns <= rx_a[0][0].monotonic_ns <= ts_end.monotonic_ns
    assert ts_begin.system_ns <= rx_a[0][0].system_ns <= ts_end.system_ns
    assert rx_a[0][1].loopback
    assert rx_a[0][1].frame.identifier == 0xBADC0FE
    assert rx_a[0][1].frame.data == b"123"

    media_a.close()
    media_b.close()
    media_a.close()  # Ensure idempotency.
    media_b.close()
Пример #2
0
    async def send(self, frames: typing.Iterable[Envelope], monotonic_deadline: float) -> int:
        del monotonic_deadline  # Unused
        if self._closed:
            raise pyuavcan.transport.ResourceClosedError

        if self._raise_on_send_once:
            self._raise_on_send_once, ex = None, self._raise_on_send_once
            assert isinstance(ex, Exception)
            raise ex

        frames = list(frames)
        assert len(frames) > 0, "Interface constraint violation: empty transmission set"
        assert min(map(lambda x: len(x.frame.data), frames)) >= 1, "CAN frames with empty payload are not valid"
        # The media interface spec says that it is guaranteed that the CAN ID is the same across the set; enforce this.
        assert len(set(map(lambda x: x.frame.identifier, frames))) == 1, "Interface constraint violation: nonuniform ID"

        timestamp = Timestamp.now()

        # Broadcast across the virtual bus we're emulating here.
        for p in self._peers:
            if p is not self:
                # Unconditionally clear the loopback flag because for the other side these are
                # regular received frames, not loopback frames.
                p._receive(  # pylint: disable=protected-access
                    (timestamp, Envelope(f.frame, loopback=False)) for f in frames
                )

        # Simple loopback emulation with acceptance filtering.
        self._receive((timestamp, f) for f in frames if f.loopback)
        return len(frames)
Пример #3
0
 def inject_received(
         self, frames: typing.Iterable[typing.Union[Envelope,
                                                    DataFrame]]) -> None:
     timestamp = Timestamp.now()
     self._receive((
         timestamp,
         (f if isinstance(f, Envelope
                          ) else Envelope(frame=f, loopback=False)),
     ) for f in frames)
Пример #4
0
 def _read_batch(self) -> typing.List[typing.Tuple[Timestamp, Envelope]]:
     batch: typing.List[typing.Tuple[Timestamp, Envelope]] = []
     while not self._closed:
         msg = self._bus.recv(0.0 if batch else self._MAXIMAL_TIMEOUT_SEC)
         if msg is None:
             break
         timestamp = Timestamp.now()  # TODO: use accurate timestamping
         loopback = False  # TODO: no possibility to get real loopback yet
         frame = self._parse_native_frame(msg)
         if frame is not None:
             batch.append((timestamp, Envelope(frame, loopback)))
     return batch
Пример #5
0
    def _read_frame(self, ts_mono_ns: int) -> typing.Tuple[Timestamp, Envelope]:
        while True:
            data, ancdata, msg_flags, _addr = self._sock.recvmsg(
                self._native_frame_size, self._ancillary_data_buffer_size
            )
            assert msg_flags & socket.MSG_TRUNC == 0, "The data buffer is not large enough"
            assert msg_flags & socket.MSG_CTRUNC == 0, "The ancillary data buffer is not large enough"

            loopback = bool(msg_flags & socket.MSG_CONFIRM)
            ts_system_ns = 0
            for cmsg_level, cmsg_type, cmsg_data in ancdata:
                if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMP:
                    sec, usec = _TIMEVAL_STRUCT.unpack(cmsg_data)
                    ts_system_ns = (sec * 1_000_000 + usec) * 1000
                else:
                    assert False, f"Unexpected ancillary data: {cmsg_level}, {cmsg_type}, {cmsg_data!r}"

            assert ts_system_ns > 0, "Missing the timestamp; does the driver support timestamping?"
            timestamp = Timestamp(system_ns=ts_system_ns, monotonic_ns=ts_mono_ns)
            out = SocketCANMedia._parse_native_frame(data)
            if out is not None:
                return timestamp, Envelope(out, loopback=loopback)
Пример #6
0
async def _unittest_can_pythoncan() -> None:
    asyncio.get_running_loop().slow_callback_duration = 5.0

    media_a = PythonCANMedia("virtual:0", 500000)
    media_b = PythonCANMedia("virtual:0", 500000)

    assert media_a.mtu == 8
    assert media_b.mtu == 8
    assert media_a.interface_name == "virtual:0"
    assert media_b.interface_name == "virtual:0"
    assert media_a.number_of_acceptance_filters == media_b.number_of_acceptance_filters
    assert media_a._maybe_thread is None
    assert media_b._maybe_thread is None

    rx_a: typing.List[typing.Tuple[Timestamp, Envelope]] = []
    rx_b: typing.List[typing.Tuple[Timestamp, Envelope]] = []

    def on_rx_a(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None:
        nonlocal rx_a
        frames = list(frames)
        print("RX A:", frames)
        rx_a += frames

    def on_rx_b(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None:
        nonlocal rx_b
        frames = list(frames)
        print("RX B:", frames)
        rx_b += frames

    media_a.start(on_rx_a, False)
    media_b.start(on_rx_b, False)

    assert media_a._maybe_thread is not None
    assert media_b._maybe_thread is not None

    await asyncio.sleep(2.0)  # This wait is needed to ensure that the RX thread handles read timeout properly

    ts_begin = Timestamp.now()
    await media_b.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 0xBADC0FE, bytearray(range(8))), loopback=True),
            Envelope(DataFrame(FrameFormat.EXTENDED, 0x12345678, bytearray(range(0))), loopback=False),
            Envelope(DataFrame(FrameFormat.BASE, 0x123, bytearray(range(6))), loopback=True),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    await asyncio.sleep(0.1)
    ts_end = Timestamp.now()

    print("rx_a:", rx_a)
    # Three received from another part
    assert len(rx_a) == 3
    for ts, _f in rx_a:
        assert ts_begin.monotonic_ns <= ts.monotonic_ns <= ts_end.monotonic_ns
        assert ts_begin.system_ns <= ts.system_ns <= ts_end.system_ns

    rx_external = list(filter(lambda x: True, rx_a))

    assert rx_external[0][1].frame.identifier == 0xBADC0FE
    assert rx_external[0][1].frame.data == bytearray(range(8))
    assert rx_external[0][1].frame.format == FrameFormat.EXTENDED

    assert rx_external[1][1].frame.identifier == 0x12345678
    assert rx_external[1][1].frame.data == bytearray(range(0))
    assert rx_external[1][1].frame.format == FrameFormat.EXTENDED

    assert rx_external[2][1].frame.identifier == 0x123
    assert rx_external[2][1].frame.data == bytearray(range(6))
    assert rx_external[2][1].frame.format == FrameFormat.BASE

    print("rx_b:", rx_b)
    # Two messages are loopback and were copied
    assert len(rx_b) == 2

    rx_loopback = list(filter(lambda x: True, rx_b))

    assert rx_loopback[0][1].frame.identifier == 0xBADC0FE
    assert rx_loopback[0][1].frame.data == bytearray(range(8))
    assert rx_loopback[0][1].frame.format == FrameFormat.EXTENDED

    assert rx_loopback[1][1].frame.identifier == 0x123
    assert rx_loopback[1][1].frame.data == bytearray(range(6))
    assert rx_loopback[1][1].frame.format == FrameFormat.BASE

    media_a.close()
    media_b.close()
Пример #7
0
async def _unittest_can_mock_media() -> None:
    peers: typing.Set[MockMedia] = set()

    me = MockMedia(peers, 64, 3)
    assert len(peers) == 1 and me in peers
    assert me.mtu == 64
    assert me.number_of_acceptance_filters == 3
    assert not me.automatic_retransmission_enabled
    assert str(me) == f"MockMedia('mock@{id(peers):08x}', mtu=64)"

    me_collector = FrameCollector()
    me.start(me_collector.give, False)
    assert me.automatic_retransmission_enabled

    # Will drop the loopback because of the acceptance filters
    await me.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")),
                     loopback=False),
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")),
                     loopback=True),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    assert me_collector.empty

    me.configure_acceptance_filters([FilterConfiguration.new_promiscuous()])
    # Now the loopback will be accepted because we have reconfigured the filters
    await me.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")),
                     loopback=False),
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")),
                     loopback=True),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    assert me_collector.pop()[1].frame == DataFrame(FrameFormat.EXTENDED, 123,
                                                    bytearray(b"def"))
    assert me_collector.empty

    pe = MockMedia(peers, 8, 1)
    assert peers == {me, pe}

    pe_collector = FrameCollector()
    pe.start(pe_collector.give, False)

    me.raise_on_send_once(RuntimeError("Hello world!"))
    with pytest.raises(RuntimeError, match="Hello world!"):
        await me.send([], asyncio.get_event_loop().time() + 1.0)

    await me.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")),
                     loopback=False),
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")),
                     loopback=True),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    assert pe_collector.empty

    pe.configure_acceptance_filters([FilterConfiguration(123, 127, None)])
    await me.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")),
                     loopback=False),
            Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")),
                     loopback=True),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    await me.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 456, bytearray(b"ghi")),
                     loopback=False),  # Dropped by the filters
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    assert pe_collector.pop()[1].frame == DataFrame(FrameFormat.EXTENDED, 123,
                                                    bytearray(b"abc"))
    assert pe_collector.pop()[1].frame == DataFrame(FrameFormat.EXTENDED, 123,
                                                    bytearray(b"def"))
    assert pe_collector.empty

    me.close()
    me.close()  # Idempotency.
    assert peers == {pe}
    with pytest.raises(pyuavcan.transport.ResourceClosedError):
        await me.send([], asyncio.get_event_loop().time() + 1.0)
    with pytest.raises(pyuavcan.transport.ResourceClosedError):
        me.configure_acceptance_filters([])
    await asyncio.sleep(
        1
    )  # Let all pending tasks finalize properly to avoid stack traces in the output.
Пример #8
0
async def _unittest_can_socketcan() -> None:
    from pyuavcan.transport import Timestamp
    from pyuavcan.transport.can.media import Envelope, DataFrame, FrameFormat, FilterConfiguration

    from pyuavcan.transport.can.media.socketcan import SocketCANMedia

    available = SocketCANMedia.list_available_interface_names()
    print("Available SocketCAN ifaces:", available)
    assert "vcan0" in available, (
        "Either the interface listing method is not working or the environment is not configured correctly. "
        'Please ensure that the virtual SocketCAN interface "vcan0" is available, and its MTU is set to 64+8.'
    )

    media_a = SocketCANMedia("vcan0", 12)
    media_b = SocketCANMedia("vcan0", 64)

    assert media_a.mtu == 12
    assert media_b.mtu == 64
    assert media_a.interface_name == "vcan0"
    assert media_b.interface_name == "vcan0"
    assert media_a.number_of_acceptance_filters == media_b.number_of_acceptance_filters
    assert media_a._maybe_thread is None  # pylint: disable=protected-access
    assert media_b._maybe_thread is None  # pylint: disable=protected-access

    media_a.configure_acceptance_filters(
        [FilterConfiguration.new_promiscuous()])
    media_b.configure_acceptance_filters(
        [FilterConfiguration.new_promiscuous()])

    rx_a: typing.List[typing.Tuple[Timestamp, Envelope]] = []

    def on_rx_a(
            frames: typing.Iterable[typing.Tuple[Timestamp,
                                                 Envelope]]) -> None:
        nonlocal rx_a
        frames = list(frames)
        print("RX A:", frames)
        rx_a += frames

    def on_rx_b(
            frames: typing.Iterable[typing.Tuple[Timestamp,
                                                 Envelope]]) -> None:
        frames = list(frames)
        print("RX B:", frames)
        asyncio.ensure_future(
            media_b.send((e for _, e in frames),
                         asyncio.get_event_loop().time() + 1.0))

    media_a.start(on_rx_a, False)
    media_b.start(on_rx_b, True)

    assert media_a._maybe_thread is not None  # pylint: disable=protected-access
    assert media_b._maybe_thread is not None  # pylint: disable=protected-access

    await asyncio.sleep(
        2.0
    )  # This wait is needed to ensure that the RX thread handles select() timeout properly

    ts_begin = Timestamp.now()
    await media_a.send(
        [
            Envelope(DataFrame(FrameFormat.BASE, 0x123, bytearray(range(6))),
                     loopback=True),
            Envelope(DataFrame(FrameFormat.EXTENDED, 0x1BADC0FE,
                               bytearray(range(8))),
                     loopback=True),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    await media_a.send(
        [
            Envelope(DataFrame(FrameFormat.EXTENDED, 0x1FF45678,
                               bytearray(range(0))),
                     loopback=False),
        ],
        asyncio.get_event_loop().time() + 1.0,
    )
    await asyncio.sleep(1.0)
    ts_end = Timestamp.now()

    print("rx_a:", rx_a)
    # Three sent back from the other end, two loopback
    assert len(rx_a) == 5
    for t, _ in rx_a:
        assert ts_begin.monotonic_ns <= t.monotonic_ns <= ts_end.monotonic_ns
        assert ts_begin.system_ns <= t.system_ns <= ts_end.system_ns

    rx_loopback = [e.frame for t, e in rx_a if e.loopback]
    rx_external = [e.frame for t, e in rx_a if not e.loopback]
    assert len(rx_loopback) == 2 and len(rx_external) == 3

    assert rx_loopback[0].identifier == 0x123
    assert rx_loopback[0].data == bytearray(range(6))
    assert rx_loopback[0].format == FrameFormat.BASE

    assert rx_loopback[1].identifier == 0x1BADC0FE
    assert rx_loopback[1].data == bytearray(range(8))
    assert rx_loopback[1].format == FrameFormat.EXTENDED

    assert rx_external[0].identifier == 0x123
    assert rx_external[0].data == bytearray(range(6))
    assert rx_external[0].format == FrameFormat.BASE

    assert rx_external[1].identifier == 0x1BADC0FE
    assert rx_external[1].data == bytearray(range(8))
    assert rx_external[1].format == FrameFormat.EXTENDED

    assert rx_external[2].identifier == 0x1FF45678
    assert rx_external[2].data == bytearray(range(0))
    assert rx_external[2].format == FrameFormat.EXTENDED

    media_a.close()
    media_b.close()

    await asyncio.sleep(
        1
    )  # Let all pending tasks finalize properly to avoid stack traces in the output.
Пример #9
0
async def _unittest_can_transport_non_anon(caplog: typing.Any) -> None:
    from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer, TransferFrom
    from pyuavcan.transport import UnsupportedSessionConfigurationError, Priority, SessionStatistics, Timestamp
    from pyuavcan.transport import ResourceClosedError, InputSessionSpecifier, OutputSessionSpecifier
    from pyuavcan.transport.can._identifier import MessageCANID, ServiceCANID
    from pyuavcan.transport.can._frame import UAVCANFrame
    from pyuavcan.transport.can.media import Envelope
    from .media.mock import MockMedia, FrameCollector

    asyncio.get_running_loop().slow_callback_duration = 5.0

    peers: typing.Set[MockMedia] = set()
    media = MockMedia(peers, 64, 10)
    media2 = MockMedia(peers, 64, 3)
    peeper = MockMedia(peers, 64, 10)
    assert len(peers) == 3

    tr = can.CANTransport(media, 5)
    tr2 = can.CANTransport(media2, 123)

    assert tr.protocol_parameters == pyuavcan.transport.ProtocolParameters(
        transfer_id_modulo=32, max_nodes=128, mtu=63)
    assert tr.local_node_id == 5
    assert tr.protocol_parameters == tr2.protocol_parameters

    assert media.automatic_retransmission_enabled
    assert media2.automatic_retransmission_enabled

    #
    # Instantiate session objects
    #
    meta = PayloadMetadata(10000)

    with pytest.raises(Exception):  # Can't broadcast service calls
        tr.get_output_session(
            OutputSessionSpecifier(
                ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE),
                None), meta)

    with pytest.raises(
            UnsupportedSessionConfigurationError):  # Can't unicast messages
        tr.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(1234), 123), meta)

    broadcaster = tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    assert broadcaster is tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    subscriber_promiscuous = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), None), meta)
    assert subscriber_promiscuous is tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), None), meta)

    subscriber_selective = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), 123), meta)

    server_listener = tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            None), meta)

    server_responder = tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)

    client_requester = tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 123),
        meta)

    client_listener = tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)

    assert set(tr.input_sessions) == {
        subscriber_promiscuous, subscriber_selective, server_listener,
        client_listener
    }
    assert set(tr.output_sessions) == {
        broadcaster, server_responder, client_requester
    }

    #
    # Basic exchange test, no one is listening
    #
    media2.configure_acceptance_filters(
        [can.media.FilterConfiguration.new_promiscuous()])
    peeper.configure_acceptance_filters(
        [can.media.FilterConfiguration.new_promiscuous()])

    collector = FrameCollector()
    peeper.start(collector.give, False)

    assert tr.sample_statistics() == can.CANTransportStatistics()
    assert tr2.sample_statistics() == can.CANTransportStatistics()

    ts = Timestamp.now()

    def validate_timestamp(timestamp: Timestamp) -> None:
        assert ts.monotonic_ns <= timestamp.monotonic_ns <= time.monotonic_ns()
        assert ts.system_ns <= timestamp.system_ns <= time.time_ns()

    assert await broadcaster.send(
        Transfer(
            timestamp=ts,
            priority=Priority.IMMEDIATE,
            transfer_id=32 + 11,  # Modulus 11
            fragmented_payload=[_mem("abc"), _mem("def")],
        ),
        tr.loop.time() + 1.0,
    )
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=6)

    assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=1)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=1, in_frames_uavcan=1)
    assert tr.sample_statistics(
    ).media_acceptance_filtering_efficiency == pytest.approx(1)
    assert tr2.sample_statistics(
    ).media_acceptance_filtering_efficiency == pytest.approx(0)
    assert tr.sample_statistics().lost_loopback_frames == 0
    assert tr2.sample_statistics().lost_loopback_frames == 0

    assert (collector.pop()[1].frame == UAVCANFrame(
        identifier=MessageCANID(Priority.IMMEDIATE, 5,
                                2345).compile([_mem("abcdef")
                                               ]),  # payload fragments joined
        padded_payload=_mem("abcdef"),
        transfer_id=11,
        start_of_transfer=True,
        end_of_transfer=True,
        toggle_bit=True,
    ).compile())
    assert collector.empty

    #
    # Broadcast exchange with input dispatch test
    #
    selective_m2345_5 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 5), meta)
    selective_m2345_9 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 9), meta)
    promiscuous_m2345 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    assert await broadcaster.send(
        Transfer(
            timestamp=ts,
            priority=Priority.IMMEDIATE,
            transfer_id=32 + 11,  # Modulus 11
            fragmented_payload=[_mem("abc"), _mem("def")],
        ),
        tr.loop.time() + 1.0,
    )
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=2, frames=2, payload_bytes=12)

    assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=2)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=2, in_frames_uavcan=2, in_frames_uavcan_accepted=1)

    received = await promiscuous_m2345.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 11
    assert received.source_node_id == 5
    assert received.priority == Priority.IMMEDIATE
    validate_timestamp(received.timestamp)
    assert received.fragmented_payload == [_mem("abcdef")]

    assert selective_m2345_5.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert selective_m2345_9.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert promiscuous_m2345.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=6)

    assert media.automatic_retransmission_enabled
    assert media2.automatic_retransmission_enabled

    feedback_collector = _FeedbackCollector()

    broadcaster.enable_feedback(feedback_collector.give)
    assert await broadcaster.send(
        Transfer(
            timestamp=ts,
            priority=Priority.SLOW,
            transfer_id=2,
            fragmented_payload=[_mem("qwe"), _mem("rty")] *
            50,  # Lots of data here, very multiframe
        ),
        tr.loop.time() + 1.0,
    )
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=3, frames=7, payload_bytes=312)
    broadcaster.disable_feedback()

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=7, out_frames_loopback=1, in_frames_loopback=1)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=7, in_frames_uavcan=7, in_frames_uavcan_accepted=6)

    fb = feedback_collector.take()
    assert fb.original_transfer_timestamp == ts
    validate_timestamp(fb.first_frame_transmission_timestamp)

    received = await promiscuous_m2345.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 2
    assert received.source_node_id == 5
    assert received.priority == Priority.SLOW
    validate_timestamp(received.timestamp)
    assert b"".join(
        received.fragmented_payload
    ) == b"qwerty" * 50 + b"\x00" * 13  # The 0x00 at the end is padding

    assert await broadcaster.send(
        Transfer(timestamp=ts,
                 priority=Priority.OPTIONAL,
                 transfer_id=3,
                 fragmented_payload=[_mem("qwe"), _mem("rty")]),
        tr.loop.time() + 1.0,
    )
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=4, frames=8, payload_bytes=318)

    received = await promiscuous_m2345.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 3
    assert received.source_node_id == 5
    assert received.priority == Priority.OPTIONAL
    validate_timestamp(received.timestamp)
    assert list(received.fragmented_payload) == [_mem("qwerty")]

    assert promiscuous_m2345.sample_statistics() == SessionStatistics(
        transfers=3, frames=7, payload_bytes=325)

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=8, out_frames_loopback=1, in_frames_loopback=1)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=8, in_frames_uavcan=8, in_frames_uavcan_accepted=7)

    broadcaster.close()
    with pytest.raises(ResourceClosedError):
        assert await broadcaster.send(
            Transfer(timestamp=ts,
                     priority=Priority.LOW,
                     transfer_id=4,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0)
    broadcaster.close()  # Does nothing

    # Final checks for the broadcaster - make sure nothing is left in the queue
    assert (await
            promiscuous_m2345.receive(tr.loop.time() + _RX_TIMEOUT)) is None

    # The selective listener was not supposed to pick up anything because it's selective for node 9, not 5
    assert (await
            selective_m2345_9.receive(tr.loop.time() + _RX_TIMEOUT)) is None

    # Now, there are a bunch of items awaiting in the selective input for node 5, collect them and check the stats
    assert selective_m2345_5.source_node_id == 5

    received = await selective_m2345_5.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 11
    assert received.source_node_id == 5
    assert received.priority == Priority.IMMEDIATE
    validate_timestamp(received.timestamp)
    assert received.fragmented_payload == [_mem("abcdef")]

    received = await selective_m2345_5.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 2
    assert received.source_node_id == 5
    assert received.priority == Priority.SLOW
    validate_timestamp(received.timestamp)
    assert b"".join(
        received.fragmented_payload
    ) == b"qwerty" * 50 + b"\x00" * 13  # The 0x00 at the end is padding

    received = await selective_m2345_5.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 3
    assert received.source_node_id == 5
    assert received.priority == Priority.OPTIONAL
    validate_timestamp(received.timestamp)
    assert list(received.fragmented_payload) == [_mem("qwerty")]

    assert selective_m2345_5.sample_statistics(
    ) == promiscuous_m2345.sample_statistics()

    #
    # Unicast exchange test
    #
    selective_server_s333_5 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 5),
        meta)
    selective_server_s333_9 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 9),
        meta)
    promiscuous_server_s333 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            None), meta)

    selective_client_s333_5 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 5),
        meta)
    selective_client_s333_9 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 9),
        meta)
    promiscuous_client_s333 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            None), meta)

    assert await client_requester.send(
        Transfer(timestamp=ts,
                 priority=Priority.FAST,
                 transfer_id=11,
                 fragmented_payload=[]),
        tr.loop.time() + 1.0)
    assert client_requester.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=0)

    received = await selective_server_s333_5.receive(tr.loop.time() + 1.0
                                                     )  # Same thing here
    assert received is not None
    assert received.transfer_id == 11
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert list(map(bytes, received.fragmented_payload)) == [b""]

    assert (await selective_server_s333_9.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None

    received = await promiscuous_server_s333.receive(tr.loop.time() + 1.0
                                                     )  # Same thing here
    assert received is not None
    assert received.transfer_id == 11
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert list(map(bytes, received.fragmented_payload)) == [b""]

    assert selective_server_s333_5.sample_statistics() == SessionStatistics(
        transfers=1, frames=1)
    assert selective_server_s333_9.sample_statistics() == SessionStatistics()
    assert promiscuous_server_s333.sample_statistics() == SessionStatistics(
        transfers=1, frames=1)

    assert (await selective_client_s333_5.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None
    assert (await selective_client_s333_9.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None
    assert (await promiscuous_client_s333.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None
    assert selective_client_s333_5.sample_statistics() == SessionStatistics()
    assert selective_client_s333_9.sample_statistics() == SessionStatistics()
    assert promiscuous_client_s333.sample_statistics() == SessionStatistics()

    client_requester.enable_feedback(
        feedback_collector.give)  # FEEDBACK ENABLED HERE

    # Will fail with an error; make sure it's counted properly. The feedback registry entry will remain pending!
    media.raise_on_send_once(RuntimeError("Induced failure"))
    with pytest.raises(RuntimeError, match="Induced failure"):
        assert await client_requester.send(
            Transfer(timestamp=ts,
                     priority=Priority.FAST,
                     transfer_id=12,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0)
    assert client_requester.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=0, errors=1)

    # Some malformed feedback frames which will be ignored
    media.inject_received([
        Envelope(
            UAVCANFrame(
                identifier=ServiceCANID(
                    priority=Priority.FAST,
                    source_node_id=5,
                    destination_node_id=123,
                    service_id=333,
                    request_not_response=True,
                ).compile([_mem("Ignored")]),
                padded_payload=_mem("Ignored"),
                start_of_transfer=False,  # Ignored because not start-of-frame
                end_of_transfer=False,
                toggle_bit=True,
                transfer_id=12,
            ).compile(),
            loopback=True,
        )
    ])
    media.inject_received([
        Envelope(
            UAVCANFrame(
                identifier=ServiceCANID(
                    priority=Priority.FAST,
                    source_node_id=5,
                    destination_node_id=123,
                    service_id=333,
                    request_not_response=True,
                ).compile([_mem("Ignored")]),
                padded_payload=_mem("Ignored"),
                start_of_transfer=True,
                end_of_transfer=False,
                toggle_bit=True,
                transfer_id=9,
            ).compile(
            ),  # Ignored because there is no such transfer-ID in the registry
            loopback=True,
        )
    ])

    # Now, this transmission will succeed, but a pending loopback registry entry will be overwritten, which will be
    # reflected in the error counter.
    with caplog.at_level(logging.CRITICAL,
                         logger=pyuavcan.transport.can.__name__):
        assert await client_requester.send(
            Transfer(
                timestamp=ts,
                priority=Priority.FAST,
                transfer_id=12,
                fragmented_payload=[
                    _mem(
                        "Until philosophers are kings, or the kings and princes of this world have the spirit and "
                        "power of philosophy, and political greatness and wisdom meet in one, and those commoner "
                        "natures who pursue either to the exclusion of the other are compelled to stand aside, "
                        "cities will never have rest from their evils "),
                    _mem("- no, nor the human race, as I believe - "),
                    _mem(
                        "and then only will this our State have a possibility of life and behold the light of day."
                    ),
                ],
            ),
            tr.loop.time() + 1.0,
        )
    client_requester.disable_feedback()
    assert client_requester.sample_statistics() == SessionStatistics(
        transfers=2, frames=8, payload_bytes=438, errors=2)

    # The feedback is disabled, but we will send a valid loopback frame anyway to make sure it is silently ignored
    media.inject_received([
        Envelope(
            UAVCANFrame(
                identifier=ServiceCANID(
                    priority=Priority.FAST,
                    source_node_id=5,
                    destination_node_id=123,
                    service_id=333,
                    request_not_response=True,
                ).compile([_mem("Ignored")]),
                padded_payload=_mem("Ignored"),
                start_of_transfer=True,
                end_of_transfer=False,
                toggle_bit=True,
                transfer_id=12,
            ).compile(),
            loopback=True,
        )
    ])

    client_requester.close()
    with pytest.raises(ResourceClosedError):
        assert await client_requester.send(
            Transfer(timestamp=ts,
                     priority=Priority.LOW,
                     transfer_id=4,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0)

    fb = feedback_collector.take()
    assert fb.original_transfer_timestamp == ts
    validate_timestamp(fb.first_frame_transmission_timestamp)

    received = await promiscuous_server_s333.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.source_node_id == 5
    assert received.transfer_id == 12
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert len(received.fragmented_payload) == 7  # Equals the number of frames
    assert sum(map(
        len, received.fragmented_payload)) == 438 + 1  # Padding also included
    assert b"Until philosophers are kings" in bytes(
        received.fragmented_payload[0])
    assert b"behold the light of day." in bytes(
        received.fragmented_payload[-1])

    received = await selective_server_s333_5.receive(tr.loop.time() + 1.0
                                                     )  # Same thing here
    assert received is not None
    assert received.transfer_id == 12
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert len(received.fragmented_payload) == 7  # Equals the number of frames
    assert sum(map(
        len, received.fragmented_payload)) == 438 + 1  # Padding also included
    assert b"Until philosophers are kings" in bytes(
        received.fragmented_payload[0])
    assert b"behold the light of day." in bytes(
        received.fragmented_payload[-1])

    # Nothing is received - non-matching node ID selector
    assert (await selective_server_s333_9.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None

    # Nothing is received - non-matching role (not server)
    assert (await selective_client_s333_5.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None
    assert (await selective_client_s333_9.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None
    assert (await promiscuous_client_s333.receive(tr.loop.time() +
                                                  _RX_TIMEOUT)) is None
    assert selective_client_s333_5.sample_statistics() == SessionStatistics()
    assert selective_client_s333_9.sample_statistics() == SessionStatistics()
    assert promiscuous_client_s333.sample_statistics() == SessionStatistics()

    # Final transport stats check; additional loopback frames are due to our manual tests above
    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16, out_frames_loopback=2, in_frames_loopback=5)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15)

    #
    # Drop non-UAVCAN frames silently
    #
    media.inject_received([
        can.media.DataFrame(
            identifier=ServiceCANID(
                priority=Priority.FAST,
                source_node_id=5,
                destination_node_id=123,
                service_id=333,
                request_not_response=True,
            ).compile([_mem("")]),
            data=bytearray(
                b""
            ),  # The CAN ID is valid for UAVCAN, but the payload is not - no tail byte
            format=can.media.FrameFormat.EXTENDED,
        )
    ])

    media.inject_received([
        can.media.DataFrame(
            identifier=0,  # The CAN ID is not valid for UAVCAN
            data=bytearray(b"123"),
            format=can.media.FrameFormat.BASE,
        )
    ])

    media.inject_received([
        Envelope(
            UAVCANFrame(
                identifier=ServiceCANID(
                    priority=Priority.FAST,
                    source_node_id=5,
                    destination_node_id=123,
                    service_id=444,  # No such service
                    request_not_response=True,
                ).compile([_mem("Ignored")]),
                padded_payload=_mem("Ignored"),
                start_of_transfer=True,
                end_of_transfer=False,
                toggle_bit=True,
                transfer_id=12,
            ).compile(),
            loopback=True,
        )
    ])

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16,
        in_frames=2,
        out_frames_loopback=2,
        in_frames_loopback=6)

    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15)

    #
    # Reception logic test.
    #
    pub_m2222 = tr2.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2222), None), meta)

    # Transfer ID timeout configuration - one of them will be configured very short for testing purposes
    subscriber_promiscuous.transfer_id_timeout = 1e-9  # Very low, basically zero timeout
    with pytest.raises(ValueError):
        subscriber_promiscuous.transfer_id_timeout = -1
    with pytest.raises(ValueError):
        subscriber_promiscuous.transfer_id_timeout = float("nan")
    assert subscriber_promiscuous.transfer_id_timeout == pytest.approx(1e-9)

    subscriber_selective.transfer_id_timeout = 1.0
    with pytest.raises(ValueError):
        subscriber_selective.transfer_id_timeout = -1
    with pytest.raises(ValueError):
        subscriber_selective.transfer_id_timeout = float("nan")
    assert subscriber_selective.transfer_id_timeout == pytest.approx(1.0)

    # Queue capacity configuration
    assert subscriber_selective.frame_queue_capacity is None  # Unlimited by default
    subscriber_selective.frame_queue_capacity = 2
    with pytest.raises(ValueError):
        subscriber_selective.frame_queue_capacity = 0
    assert subscriber_selective.frame_queue_capacity == 2

    assert await pub_m2222.send(
        Transfer(
            timestamp=ts,
            priority=Priority.EXCEPTIONAL,
            transfer_id=7,
            fragmented_payload=[
                _mem("Finally, from so little sleeping and so much reading, "),
                _mem(
                    "his brain dried up and he went completely out of his mind."
                ),  # Two frames.
            ],
        ),
        tr.loop.time() + 1.0,
    )

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16,
        in_frames=4,
        in_frames_uavcan=2,
        in_frames_uavcan_accepted=2,
        out_frames_loopback=2,
        in_frames_loopback=6,
    )

    assert tr2.sample_statistics() == can.CANTransportStatistics(
        out_frames=2,
        in_frames=16,
        in_frames_uavcan=16,
        in_frames_uavcan_accepted=15)

    received = await subscriber_promiscuous.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.source_node_id == 123
    assert received.priority == Priority.EXCEPTIONAL
    assert received.transfer_id == 7
    validate_timestamp(received.timestamp)
    assert bytes(received.fragmented_payload[0]).startswith(b"Finally")
    assert bytes(received.fragmented_payload[-1]).rstrip(b"\x00").endswith(
        b"out of his mind.")

    received = await subscriber_selective.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert received.priority == Priority.EXCEPTIONAL
    assert received.transfer_id == 7
    validate_timestamp(received.timestamp)
    assert bytes(received.fragmented_payload[0]).startswith(b"Finally")
    assert bytes(received.fragmented_payload[-1]).rstrip(b"\x00").endswith(
        b"out of his mind.")

    assert subscriber_selective.sample_statistics(
    ) == subscriber_promiscuous.sample_statistics()
    assert subscriber_promiscuous.sample_statistics() == SessionStatistics(
        transfers=1, frames=2, payload_bytes=124)  # Includes padding!

    # Small delay is needed to make the small-TID instance certainly time out on Windows, where clock resolution is low.
    await asyncio.sleep(0.1)
    assert await pub_m2222.send(
        Transfer(
            timestamp=ts,
            priority=Priority.NOMINAL,
            transfer_id=
            7,  # Same transfer ID, will be accepted only by the instance with low TID timeout
            fragmented_payload=[],
        ),
        tr.loop.time() + 1.0,
    )

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16,
        in_frames=5,
        in_frames_uavcan=3,
        in_frames_uavcan_accepted=3,
        out_frames_loopback=2,
        in_frames_loopback=6,
    )

    assert tr2.sample_statistics() == can.CANTransportStatistics(
        out_frames=3,
        in_frames=16,
        in_frames_uavcan=16,
        in_frames_uavcan_accepted=15)

    received = await subscriber_promiscuous.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.source_node_id == 123
    assert received.priority == Priority.NOMINAL
    assert received.transfer_id == 7
    validate_timestamp(received.timestamp)
    assert b"".join(received.fragmented_payload) == b""

    assert subscriber_promiscuous.sample_statistics() == SessionStatistics(
        transfers=2, frames=3, payload_bytes=124)

    # Discarded because of the same transfer ID
    assert (await
            subscriber_selective.receive(tr.loop.time() + _RX_TIMEOUT)) is None
    assert subscriber_selective.sample_statistics() == SessionStatistics(
        transfers=1,
        frames=3,
        payload_bytes=124,
        errors=1  # Error due to the repeated transfer ID
    )

    assert await pub_m2222.send(
        Transfer(
            timestamp=ts,
            priority=Priority.HIGH,
            transfer_id=8,
            fragmented_payload=[
                _mem("a" * 63),
                _mem("b" * 63),
                _mem("c" * 63),
                _mem(
                    "d" * 62
                ),  # Tricky case - one of the CRC bytes spills over into the fifth frame
            ],
        ),
        tr.loop.time() + 1.0,
    )

    # The promiscuous one is able to receive the transfer since its queue is large enough
    received = await subscriber_promiscuous.receive(tr.loop.time() + 1.0)
    assert received is not None
    assert received.priority == Priority.HIGH
    assert received.transfer_id == 8
    validate_timestamp(received.timestamp)
    assert list(map(bytes, received.fragmented_payload)) == [
        b"a" * 63,
        b"b" * 63,
        b"c" * 63,
        b"d" * 62,
    ]
    assert subscriber_promiscuous.sample_statistics() == SessionStatistics(
        transfers=3, frames=8, payload_bytes=375)

    # The selective one is unable to do so since its RX queue is too small; it is reflected in the error counter
    assert (await
            subscriber_selective.receive(tr.loop.time() + _RX_TIMEOUT)) is None
    assert subscriber_selective.sample_statistics() == SessionStatistics(
        transfers=1, frames=5, payload_bytes=124, errors=1,
        drops=3)  # Overruns!

    #
    # Finalization.
    #
    print("str(CANTransport):", tr)
    print("str(CANTransport):", tr2)
    client_listener.close()
    server_listener.close()
    subscriber_promiscuous.close()
    subscriber_selective.close()
    tr.close()
    tr2.close()
    # Double-close has no effect:
    client_listener.close()
    server_listener.close()
    subscriber_promiscuous.close()
    subscriber_selective.close()
    tr.close()
    tr2.close()
    await asyncio.sleep(
        1
    )  # Let all pending tasks finalize properly to avoid stack traces in the output.