Ejemplo n.º 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()
Ejemplo n.º 2
0
    async def _emit(self, header_payload_pairs: typing.Sequence[typing.Tuple[
        memoryview, memoryview]],
                    monotonic_deadline: float) -> typing.Optional[Timestamp]:
        """
        Returns the transmission timestamp of the first frame (which is the transfer timestamp) on success.
        Returns None if at least one frame could not be transmitted.
        """
        ts: typing.Optional[Timestamp] = None
        loop = asyncio.get_running_loop()
        for index, (header, payload) in enumerate(header_payload_pairs):
            try:
                # TODO: concatenation is inefficient. Use vectorized IO via sendmsg() instead!
                await asyncio.wait_for(
                    loop.sock_sendall(self._sock, b"".join((header, payload))),
                    timeout=monotonic_deadline - loop.time(),
                )

                # TODO: use socket timestamping when running on Linux (Windows does not support timestamping).
                # Depending on the chosen approach, timestamping on Linux may require us to launch a new thread
                # reading from the socket's error message queue and then matching the returned frames with a
                # pending loopback registry, kind of like it's done with CAN.
                ts = ts or Timestamp.now()

            except (asyncio.TimeoutError, asyncio.CancelledError):
                self._statistics.drops += len(header_payload_pairs) - index
                return None
            except Exception as ex:
                if _IGNORE_OS_ERROR_ON_SEND and isinstance(
                        ex, OSError) and self._sock.fileno() >= 0:
                    # Windows compatibility workaround -- if there are no registered multicast receivers on the
                    # loopback interface, send() may raise WinError 1231 or 10051. This error shall be suppressed.
                    _logger.debug(
                        "%r: Socket send error ignored (the likely cause is that there are no known receivers "
                        "on the other end of the link): %r",
                        self,
                        ex,
                    )
                    # To suppress the error properly, we have to pretend that the data was actually transmitted,
                    # so we populate the timestamp with a phony value anyway.
                    ts = ts or Timestamp.now()
                else:
                    self._statistics.errors += 1
                    raise

            self._statistics.frames += 1
            self._statistics.payload_bytes += len(payload)

        return ts
Ejemplo n.º 3
0
def _unittest_validate_and_finalize_transfer() -> None:
    ts = Timestamp.now()
    prio = Priority.FAST
    tid = 888888888
    src_nid = 1234

    def mk_transfer(fp: typing.Sequence[bytes]) -> TransferFrom:
        return TransferFrom(
            timestamp=ts,
            priority=prio,
            transfer_id=tid,
            fragmented_payload=list(map(memoryview, fp)),
            source_node_id=src_nid,
        )

    def call(fp: typing.Sequence[bytes]) -> typing.Optional[TransferFrom]:
        return _validate_and_finalize_transfer(
            timestamp=ts,
            priority=prio,
            transfer_id=tid,
            frame_payloads=list(map(memoryview, fp)),
            source_node_id=src_nid,
        )

    assert call([b""]) == mk_transfer([b""])
    assert call([b"hello world"]) == mk_transfer([b"hello world"])
    assert call([
        b"hello world", b"0123456789",
        TransferCRC.new(b"hello world", b"0123456789").value_as_bytes
    ]) == mk_transfer([b"hello world", b"0123456789"])
    assert call([b"hello world", b"0123456789"]) is None  # no CRC
Ejemplo n.º 4
0
def _unittest_transfer_reassembler_anonymous() -> None:
    ts = Timestamp.now()
    prio = Priority.LOW
    assert TransferReassembler.construct_anonymous_transfer(
        ts,
        Frame(priority=prio,
              transfer_id=123456,
              index=0,
              end_of_transfer=True,
              payload=memoryview(b"abcdef")),
    ) == TransferFrom(timestamp=ts,
                      priority=prio,
                      transfer_id=123456,
                      fragmented_payload=[memoryview(b"abcdef")],
                      source_node_id=None)

    assert (TransferReassembler.construct_anonymous_transfer(
        ts,
        Frame(priority=prio,
              transfer_id=123456,
              index=1,
              end_of_transfer=True,
              payload=memoryview(b"abcdef")),
    ) is None)

    assert (TransferReassembler.construct_anonymous_transfer(
        ts,
        Frame(priority=prio,
              transfer_id=123456,
              index=0,
              end_of_transfer=False,
              payload=memoryview(b"abcdef")),
    ) is None)
Ejemplo n.º 5
0
def _unittest_frame_compile_service() -> None:
    from pyuavcan.transport import Priority, ServiceDataSpecifier, Timestamp

    f = SerialFrame(timestamp=Timestamp.now(),
                    priority=Priority.FAST,
                    source_node_id=SerialFrame.FRAME_DELIMITER_BYTE,
                    destination_node_id=None,
                    data_specifier=ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE),
                    transfer_id=1234567890123456789,
                    index=1234567,
                    end_of_transfer=False,
                    payload=memoryview(b''))

    buffer = bytearray(0 for _ in range(50))
    mv = f.compile_into(buffer)

    assert mv[0] == mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE
    segment_cobs = bytes(mv[1:-1])
    assert SerialFrame.FRAME_DELIMITER_BYTE not in segment_cobs

    segment = cobs.decode(segment_cobs)

    # Header validation
    assert segment[0] == _VERSION
    assert segment[1] == int(Priority.FAST)
    assert (segment[2], segment[3]) == (SerialFrame.FRAME_DELIMITER_BYTE, 0)
    assert (segment[4], segment[5]) == (0xFF, 0xFF)
    assert segment[6:8] == ((1 << 15) | (1 << 14) | 123).to_bytes(2, 'little')
    assert segment[8:16] == b'\x00' * 8
    assert segment[16:24] == 1234567890123456789 .to_bytes(8, 'little')
    assert segment[24:28] == 1234567 .to_bytes(4, 'little')
    # Header CRC here

    # CRC validation
    assert segment[32:] == pyuavcan.transport.commons.crc.CRC32C.new(f.payload).value_as_bytes
Ejemplo n.º 6
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)
Ejemplo n.º 7
0
def _unittest_transfer_reassembler_anonymous() -> None:
    from pyuavcan.transport import Timestamp, Priority, TransferFrom

    ts = Timestamp.now()
    prio = Priority.LOW
    assert TransferReassembler.construct_anonymous_transfer(
        Frame(timestamp=ts,
              priority=prio,
              transfer_id=123456,
              index=0,
              end_of_transfer=True,
              payload=memoryview(b'abcdef'))) == TransferFrom(
                  timestamp=ts,
                  priority=prio,
                  transfer_id=123456,
                  fragmented_payload=[memoryview(b'abcdef')],
                  source_node_id=None)

    assert TransferReassembler.construct_anonymous_transfer(
        Frame(timestamp=ts,
              priority=prio,
              transfer_id=123456,
              index=1,
              end_of_transfer=True,
              payload=memoryview(b'abcdef'))) is None

    assert TransferReassembler.construct_anonymous_transfer(
        Frame(timestamp=ts,
              priority=prio,
              transfer_id=123456,
              index=0,
              end_of_transfer=False,
              payload=memoryview(b'abcdef'))) is None
Ejemplo n.º 8
0
 async def send(self, frames: typing.Iterable[Envelope],
                monotonic_deadline: float) -> int:
     num_sent = 0
     loopback: typing.List[typing.Tuple[Timestamp, Envelope]] = []
     for f in frames:
         if self._closed:
             raise ResourceClosedError(repr(self))
         message = can.Message(
             arbitration_id=f.frame.identifier,
             is_extended_id=(f.frame.format == FrameFormat.EXTENDED),
             data=f.frame.data,
             is_fd=self._is_fd,
         )
         try:
             await self._loop.run_in_executor(
                 self._background_executor,
                 functools.partial(self._bus.send,
                                   message,
                                   timeout=monotonic_deadline -
                                   self._loop.time()),
             )
         except (asyncio.TimeoutError, can.CanError
                 ):  # CanError is also used to report timeouts (weird).
             break
         else:
             num_sent += 1
             if f.loopback:
                 loopback.append((Timestamp.now(), f))
     if loopback:
         self.loop.call_soon(self._invoke_rx_handler, loopback)
     return num_sent
Ejemplo n.º 9
0
 def callback(lls: LinkLayerCapture) -> None:
     nonlocal ts_last
     now = Timestamp.now()
     assert ts_last.monotonic_ns <= lls.timestamp.monotonic_ns <= now.monotonic_ns
     assert ts_last.system_ns <= lls.timestamp.system_ns <= now.system_ns
     ts_last = lls.timestamp
     sniffs.append(lls.packet)
Ejemplo n.º 10
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)
Ejemplo n.º 11
0
 def sniff_sniff(cap: LinkLayerCapture) -> None:
     nonlocal ts_last
     now = Timestamp.now()
     assert ts_last.monotonic_ns <= cap.timestamp.monotonic_ns <= now.monotonic_ns
     assert ts_last.system_ns <= cap.timestamp.system_ns <= now.system_ns
     ts_last = cap.timestamp
     # Make sure that all traffic from foreign networks is filtered out by the sniffer.
     assert (int(parse_ip(cap.packet).source_destination[0])
             & 0x_FFFF_0000) == (int(fac.local_ip_address) & 0x_FFFF_0000)
     sniffs.append(cap)
Ejemplo n.º 12
0
 def sniff_sniff(ts: Timestamp, pack: RawPacket) -> None:
     nonlocal ts_last
     now = Timestamp.now()
     assert ts_last.monotonic_ns <= ts.monotonic_ns <= now.monotonic_ns
     assert ts_last.system_ns <= ts.system_ns <= now.system_ns
     ts_last = ts
     # Make sure that all traffic from foreign networks is filtered out by the sniffer.
     assert (int(pack.ip_header.source)
             & 0x_FFFF_0000) == (int(fac.local_ip_address) & 0x_FFFF_0000)
     sniffs.append(pack)
Ejemplo n.º 13
0
 async def send_and_wait() -> None:
     ts = Timestamp.now()
     sock_tx.send(b"".join(
         UDPFrame(
             priority=Priority.HIGH,
             transfer_id=0,
             index=0,
             end_of_transfer=True,
             payload=memoryview(str(ts).encode()),
         ).compile_header_and_payload()))
     await (asyncio.sleep(0.5))  # Let the handler run in the background.
Ejemplo n.º 14
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
Ejemplo n.º 15
0
def _unittest_frame_compile_message() -> None:
    from pyuavcan.transport import Priority, MessageDataSpecifier, Timestamp

    f = SerialFrame(timestamp=Timestamp.now(),
                    priority=Priority.HIGH,
                    source_node_id=SerialFrame.FRAME_DELIMITER_BYTE,
                    destination_node_id=SerialFrame.ESCAPE_PREFIX_BYTE,
                    data_specifier=MessageDataSpecifier(12345),
                    data_type_hash=0xdead_beef_bad_c0ffe,
                    transfer_id=1234567890123456789,
                    index=1234567,
                    end_of_transfer=True,
                    payload=memoryview(b'abcd\x9Eef\x8E'))

    buffer = bytearray(0 for _ in range(1000))
    mv = f.compile_into(buffer)

    assert mv[0] == SerialFrame.FRAME_DELIMITER_BYTE
    assert mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE
    segment = bytes(mv[1:-1])
    assert SerialFrame.FRAME_DELIMITER_BYTE not in segment

    # Header validation
    assert segment[0] == _VERSION
    assert segment[1] == int(Priority.HIGH)
    assert segment[2] == SerialFrame.ESCAPE_PREFIX_BYTE
    assert (segment[3], segment[4]) == (SerialFrame.FRAME_DELIMITER_BYTE ^ 0xFF, 0)
    assert segment[5] == SerialFrame.ESCAPE_PREFIX_BYTE
    assert (segment[6], segment[7]) == (SerialFrame.ESCAPE_PREFIX_BYTE ^ 0xFF, 0)
    assert segment[8:10] == 12345 .to_bytes(2, 'little')
    assert segment[10:18] == 0xdead_beef_bad_c0ffe .to_bytes(8, 'little')
    assert segment[18:26] == 1234567890123456789 .to_bytes(8, 'little')
    assert segment[26:30] == (1234567 + 0x8000_0000).to_bytes(4, 'little')
    assert segment[30:34] == b'\x00' * 4

    # Payload validation
    assert segment[34:38] == b'abcd'
    assert segment[38] == SerialFrame.ESCAPE_PREFIX_BYTE
    assert segment[39] == 0x9E ^ 0xFF
    assert segment[40:42] == b'ef'
    assert segment[42] == SerialFrame.ESCAPE_PREFIX_BYTE
    assert segment[43] == 0x8E ^ 0xFF

    # CRC validation
    header = SerialFrame.HEADER_STRUCT.pack(_VERSION,
                                            int(f.priority),
                                            f.source_node_id,
                                            f.destination_node_id,
                                            12345,
                                            f.data_type_hash,
                                            f.transfer_id,
                                            f.index + 0x8000_0000)
    assert segment[44:] == pyuavcan.transport.commons.crc.CRC32C.new(header, f.payload).value_as_bytes
Ejemplo n.º 16
0
def _unittest_udp_tracer() -> None:
    from pytest import approx
    from ipaddress import ip_address
    from pyuavcan.transport import Priority, ServiceDataSpecifier
    from pyuavcan.transport.udp import UDPTransport
    from ._ip import MACHeader, IPHeader, UDPHeader, service_data_specifier_to_udp_port

    tr = UDPTransport.make_tracer()
    ts = Timestamp.now()

    ds = ServiceDataSpecifier(11, ServiceDataSpecifier.Role.RESPONSE)
    trace = tr.update(
        UDPCapture(
            ts,
            RawPacket(
                MACHeader(memoryview(b""), memoryview(b"")),
                IPHeader(ip_address("127.0.0.42"), ip_address("127.0.0.63")),
                UDPHeader(12345, service_data_specifier_to_udp_port(ds)),
                memoryview(b"".join(
                    UDPFrame(
                        priority=Priority.SLOW,
                        transfer_id=1234567890,
                        index=0,
                        end_of_transfer=True,
                        payload=memoryview(b"Hello world!"),
                    ).compile_header_and_payload())),
            ),
        ))
    assert isinstance(trace, TransferTrace)
    assert trace.timestamp == ts
    assert trace.transfer_id_timeout == approx(
        AlienTransferReassembler.MAX_TRANSFER_ID_TIMEOUT)  # Initial value.
    assert trace.transfer.metadata.transfer_id == 1234567890
    assert trace.transfer.metadata.priority == Priority.SLOW
    assert trace.transfer.metadata.session_specifier.source_node_id == 42
    assert trace.transfer.metadata.session_specifier.destination_node_id == 63
    assert trace.transfer.metadata.session_specifier.data_specifier == ds
    assert trace.transfer.fragmented_payload == [memoryview(b"Hello world!")]

    assert None is tr.update(
        pyuavcan.transport.Capture(ts))  # Another transport, ignored.

    assert None is tr.update(
        UDPCapture(  # Malformed frame.
            ts,
            RawPacket(
                MACHeader(memoryview(b""), memoryview(b"")),
                IPHeader(ip_address("127.0.0.42"), ip_address("127.1.0.63")),
                UDPHeader(1, 1),
                memoryview(b""),
            ),
        ))
Ejemplo n.º 17
0
    def _finalize(self, known_invalid: bool) -> None:
        if not self._buffer or (len(self._buffer) == 1 and self._buffer[0]
                                == SerialFrame.FRAME_DELIMITER_BYTE):
            # Avoid noise in the OOB output during normal operation.
            # TODO: this is a hack in place of the proper on-the-fly COBS parser.
            return

        buf = memoryview(self._buffer)
        self._buffer = bytearray(
        )  # There are memoryview instances pointing to the old buffer!
        ts = self._timestamp or Timestamp.now()
        self._timestamp = None

        parsed: typing.Optional[SerialFrame] = None
        if (not known_invalid) and len(buf) <= self._max_frame_size_bytes:
            parsed = SerialFrame.parse_from_cobs_image(buf)

        self._callback(ts, buf, parsed)
Ejemplo n.º 18
0
async def _unittest_issue_120() -> None:
    from pyuavcan.transport import MessageDataSpecifier, PayloadMetadata, Transfer
    from pyuavcan.transport import Priority, Timestamp, OutputSessionSpecifier
    from .media.mock import MockMedia

    asyncio.get_running_loop().slow_callback_duration = 5.0

    peers: typing.Set[MockMedia] = set()
    media = MockMedia(peers, 8, 10)
    tr = can.CANTransport(media, 42)
    assert tr.protocol_parameters.transfer_id_modulo == 32

    feedback_collector = _FeedbackCollector()

    ses = tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None),
        PayloadMetadata(1024))
    ses.enable_feedback(feedback_collector.give)
    for i in range(70):
        ts = Timestamp.now()
        assert await ses.send(
            Transfer(
                timestamp=ts,
                priority=Priority.SLOW,
                transfer_id=i,
                fragmented_payload=[_mem(str(i))] *
                7,  # Ensure both single- and multiframe
            ),
            tr.loop.time() + 1.0,
        )
        await asyncio.sleep(0.1)
        fb = feedback_collector.take()
        assert fb.original_transfer_timestamp == ts

    num_frames = (10 * 1) + (60 * 3)  # 10 single-frame, 60 multi-frame
    assert 70 == ses.sample_statistics().transfers
    assert num_frames == ses.sample_statistics().frames
    assert 0 == tr.sample_statistics().in_frames  # loopback not included here
    assert 70 == tr.sample_statistics(
    ).in_frames_loopback  # only first frame of each transfer
    assert num_frames == tr.sample_statistics().out_frames
    assert 70 == tr.sample_statistics(
    ).out_frames_loopback  # only first frame of each transfer
    assert 0 == tr.sample_statistics().lost_loopback_frames
Ejemplo n.º 19
0
def _unittest_can_capture() -> None:
    from pyuavcan.transport import MessageDataSpecifier
    from .media import FrameFormat
    from ._identifier import MessageCANID

    ts = Timestamp.now()
    payload = bytearray(b"123\x0A")
    cap = CANCapture(
        ts,
        DataFrame(
            FrameFormat.EXTENDED,
            MessageCANID(Priority.SLOW, 42,
                         3210).compile([memoryview(payload)]),
            payload,
        ),
        own=True,
    )
    print(cap)
    parsed = cap.parse()
    assert parsed is not None
    ss, prio, uf = parsed
    assert ss.source_node_id == 42
    assert ss.destination_node_id is None
    assert isinstance(ss.data_specifier, MessageDataSpecifier)
    assert ss.data_specifier.subject_id == 3210
    assert prio == Priority.SLOW
    assert uf.transfer_id == 0x0A
    assert uf.padded_payload == b"123"
    assert not uf.start_of_transfer
    assert not uf.end_of_transfer
    assert not uf.toggle_bit

    # Invalid CAN ID
    assert None is CANCapture(
        ts, DataFrame(FrameFormat.BASE, 123, payload), own=True).parse()

    # Invalid CAN payload
    assert (None is CANCapture(
        ts,
        DataFrame(FrameFormat.EXTENDED,
                  MessageCANID(Priority.SLOW, 42, 3210).compile([]),
                  bytearray()),
        own=True,
    ).parse())
Ejemplo n.º 20
0
def _unittest_frame_compile_service() -> None:
    from pyuavcan.transport import Priority, ServiceDataSpecifier, Timestamp

    f = SerialFrame(timestamp=Timestamp.now(),
                    priority=Priority.FAST,
                    source_node_id=SerialFrame.FRAME_DELIMITER_BYTE,
                    destination_node_id=None,
                    data_specifier=ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE),
                    data_type_hash=0xdead_beef_bad_c0ffe,
                    transfer_id=1234567890123456789,
                    index=1234567,
                    end_of_transfer=False,
                    payload=memoryview(b''))

    buffer = bytearray(0 for _ in range(50))
    mv = f.compile_into(buffer)

    assert mv[0] == mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE
    segment = bytes(mv[1:-1])
    assert SerialFrame.FRAME_DELIMITER_BYTE not in segment

    # Header validation
    assert segment[0] == _VERSION
    assert segment[1] == int(Priority.FAST)
    assert segment[2] == SerialFrame.ESCAPE_PREFIX_BYTE
    assert (segment[3], segment[4]) == (SerialFrame.FRAME_DELIMITER_BYTE ^ 0xFF, 0)
    assert (segment[5], segment[6]) == (0xFF, 0xFF)
    assert segment[7:9] == ((1 << 15) | (1 << 14) | 123) .to_bytes(2, 'little')
    assert segment[9:17] == 0xdead_beef_bad_c0ffe .to_bytes(8, 'little')
    assert segment[17:25] == 1234567890123456789 .to_bytes(8, 'little')
    assert segment[25:29] == 1234567 .to_bytes(4, 'little')
    assert segment[29:33] == b'\x00' * 4

    # CRC validation
    header = SerialFrame.HEADER_STRUCT.pack(_VERSION,
                                            int(f.priority),
                                            f.source_node_id,
                                            _ANONYMOUS_NODE_ID,
                                            (1 << 15) | (1 << 14) | 123,
                                            f.data_type_hash,
                                            f.transfer_id,
                                            f.index)
    assert segment[33:] == pyuavcan.transport.commons.crc.CRC32C.new(header, f.payload).value_as_bytes
Ejemplo n.º 21
0
    def _reader_thread_func(self) -> None:
        in_bytes_count = 0

        def callback(ts: Timestamp, buf: memoryview,
                     frame: typing.Optional[SerialFrame]) -> None:
            item = buf if frame is None else frame
            self._loop.call_soon_threadsafe(
                self._handle_received_item_and_update_stats, ts, item,
                in_bytes_count)
            if self._capture_handlers:
                pyuavcan.util.broadcast(self._capture_handlers)(SerialCapture(
                    ts, SerialCapture.Direction.RX, buf))

        try:
            parser = StreamParser(callback, max(self.VALID_MTU_RANGE))
            assert abs(self._serial_port.timeout -
                       _SERIAL_PORT_READ_TIMEOUT) < 0.1

            while not self._closed and self._serial_port.is_open:
                chunk = self._serial_port.read(
                    max(1, self._serial_port.inWaiting()))
                chunk_ts = Timestamp.now()
                in_bytes_count += len(chunk)
                parser.process_next_chunk(chunk, chunk_ts)

        except Exception as ex:  # pragma: no cover
            if self._closed or not self._serial_port.is_open:
                _logger.debug(
                    "%s: The serial port is closed, exception ignored: %r",
                    self, ex)
            else:
                _logger.exception(
                    "%s: Reader thread has failed, the instance with port %s will be terminated: %s",
                    self,
                    self._serial_port,
                    ex,
                )
            self._closed = True
            self._serial_port.close()

        finally:
            _logger.debug("%s: Reader thread is exiting. Head aega.", self)
def _unittest_serialize_transfer() -> None:
    from pyuavcan.transport import Priority, Timestamp

    timestamp = Timestamp.now()
    priority = Priority.NOMINAL
    transfer_id = 12345678901234567890

    def construct_frame(index: int, end_of_transfer: bool,
                        payload: memoryview) -> Frame:
        return Frame(timestamp=timestamp,
                     priority=priority,
                     transfer_id=transfer_id,
                     index=index,
                     end_of_transfer=end_of_transfer,
                     payload=payload)

    assert [
        construct_frame(0, True, memoryview(b'hello world')),
    ] == list(
        serialize_transfer(
            [memoryview(b'hello'),
             memoryview(b' '),
             memoryview(b'world')], 100, construct_frame))

    assert [
        construct_frame(0, True, memoryview(b'')),
    ] == list(serialize_transfer([], 100, construct_frame))

    hello_world_crc = pyuavcan.transport.commons.crc.CRC32C()
    hello_world_crc.add(b'hello world')

    assert [
        construct_frame(0, False, memoryview(b'hello')),
        construct_frame(1, False, memoryview(b' worl')),
        construct_frame(2, True,
                        memoryview(b'd' + hello_world_crc.value_as_bytes)),
    ] == list(
        serialize_transfer(
            [memoryview(b'hello'),
             memoryview(b' '),
             memoryview(b'world')], 5, construct_frame))
Ejemplo n.º 23
0
def _unittest_frame_compile_message() -> None:
    from pyuavcan.transport import Priority, MessageDataSpecifier, Timestamp

    f = SerialFrame(timestamp=Timestamp.now(),
                    priority=Priority.HIGH,
                    source_node_id=SerialFrame.FRAME_DELIMITER_BYTE,
                    destination_node_id=SerialFrame.FRAME_DELIMITER_BYTE,
                    data_specifier=MessageDataSpecifier(12345),
                    data_type_hash=0xdead_beef_bad_c0ffe,
                    transfer_id=1234567890123456789,
                    index=1234567,
                    end_of_transfer=True,
                    payload=memoryview(b'abcd\x00ef\x00'))

    buffer = bytearray(0 for _ in range(1000))
    mv = f.compile_into(buffer)

    assert mv[0] == SerialFrame.FRAME_DELIMITER_BYTE
    assert mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE

    segment_cobs = bytes(mv[1:-1])
    assert SerialFrame.FRAME_DELIMITER_BYTE not in segment_cobs

    segment = cobs.decode(segment_cobs)

    # Header validation
    assert segment[0] == _VERSION
    assert segment[1] == int(Priority.HIGH)
    assert (segment[2], segment[3]) == (SerialFrame.FRAME_DELIMITER_BYTE, 0)
    assert (segment[4], segment[5]) == (SerialFrame.FRAME_DELIMITER_BYTE, 0)
    assert segment[6:8] == 12345 .to_bytes(2, 'little')
    assert segment[8:16] == 0xdead_beef_bad_c0ffe .to_bytes(8, 'little')
    assert segment[16:24] == 1234567890123456789 .to_bytes(8, 'little')
    assert segment[24:28] == (1234567 + 0x8000_0000).to_bytes(4, 'little')
    # Header CRC here

    # Payload validation
    assert segment[32:40] == b'abcd\x00ef\x00'
    assert segment[40:] == pyuavcan.transport.commons.crc.CRC32C.new(f.payload).value_as_bytes
Ejemplo n.º 24
0
    async def _send_transfer(
            self, frames: typing.List[SerialFrame],
            monotonic_deadline: float) -> typing.Optional[Timestamp]:
        """
        Emits the frames belonging to the same transfer, returns the first frame transmission timestamp.
        The returned timestamp can be used for transfer feedback implementation.
        Aborts if the frames cannot be emitted before the deadline or if a write call fails.
        :returns: The first frame transmission timestamp if all frames are sent successfully.
            None on timeout or on write failure.
        """
        tx_ts: typing.Optional[Timestamp] = None
        self._ensure_not_closed()
        try:  # Jeez this is getting complex
            num_sent = 0
            for fr in frames:
                async with self._port_lock:  # TODO: the lock acquisition should be prioritized by frame priority!
                    min_buffer_size = len(fr.payload) * 3
                    if len(self._serialization_buffer) < min_buffer_size:
                        _logger.debug(
                            "%s: The serialization buffer is being enlarged from %d to %d bytes",
                            self,
                            len(self._serialization_buffer),
                            min_buffer_size,
                        )
                        self._serialization_buffer = bytearray(
                            0 for _ in range(min_buffer_size))
                    compiled = fr.compile_into(self._serialization_buffer)
                    timeout = monotonic_deadline - self._loop.time()
                    if timeout > 0:
                        self._serial_port.write_timeout = timeout
                        try:
                            num_written = await self._loop.run_in_executor(
                                self._background_executor,
                                self._serial_port.write, compiled)
                            tx_ts = tx_ts or Timestamp.now()
                        except serial.SerialTimeoutException:
                            num_written = 0
                            _logger.info(
                                "%s: Port write timed out in %.3fs on frame %r",
                                self, timeout, fr)
                        else:
                            if self._capture_handlers:  # Create a copy to decouple data from the serialization buffer!
                                cap = SerialCapture(
                                    tx_ts, SerialCapture.Direction.TX,
                                    memoryview(bytes(compiled)))
                                pyuavcan.util.broadcast(
                                    self._capture_handlers)(cap)
                        self._statistics.out_bytes += num_written or 0
                    else:
                        tx_ts = None  # Timed out
                        break

                num_written = len(
                    compiled) if num_written is None else num_written
                if num_written < len(compiled):
                    tx_ts = None  # Write failed
                    break
                num_sent += 1

            self._statistics.out_frames += num_sent
        except Exception as ex:
            if self._closed:
                raise pyuavcan.transport.ResourceClosedError(
                    f"{self} is closed, transmission aborted.") from ex
            raise
        else:
            if tx_ts is not None:
                self._statistics.out_transfers += 1
            else:
                self._statistics.out_incomplete += 1
            return tx_ts
Ejemplo n.º 25
0
def _unittest_redundant_input_monotonic() -> None:
    import pytest
    from pyuavcan.transport import Transfer, Timestamp, Priority
    from pyuavcan.transport.loopback import LoopbackTransport

    loop = asyncio.get_event_loop()
    await_ = loop.run_until_complete

    spec = pyuavcan.transport.InputSessionSpecifier(
        pyuavcan.transport.MessageDataSpecifier(4321), None)
    spec_tx = pyuavcan.transport.OutputSessionSpecifier(
        spec.data_specifier, None)
    meta = pyuavcan.transport.PayloadMetadata(30)

    ts = Timestamp.now()

    tr_a = LoopbackTransport(111)
    tr_b = LoopbackTransport(111)
    tx_a = tr_a.get_output_session(spec_tx, meta)
    tx_b = tr_b.get_output_session(spec_tx, meta)
    inf_a = tr_a.get_input_session(spec, meta)
    inf_b = tr_b.get_input_session(spec, meta)

    inf_a.transfer_id_timeout = 1.1  # This is used to ensure that the transfer-ID timeout is handled correctly.

    ses = RedundantInputSession(
        spec,
        meta,
        tid_modulo_provider=lambda:
        None,  # Like UDP or serial - infinite modulo.
        loop=loop,
        finalizer=lambda: None,
    )
    assert ses.specifier is spec
    assert ses.payload_metadata is meta
    assert not ses.inferiors
    assert ses.sample_statistics() == RedundantSessionStatistics()
    assert pytest.approx(0.0) == ses.transfer_id_timeout

    # Add inferiors.
    ses._add_inferior(inf_a)  # No change, added above    # pylint: disable=protected-access
    assert ses.inferiors == [inf_a]
    ses._add_inferior(inf_b)  # pylint: disable=protected-access
    assert ses.inferiors == [inf_a, inf_b]

    ses.transfer_id_timeout = 1.1
    assert ses.transfer_id_timeout == pytest.approx(1.1)
    assert inf_a.transfer_id_timeout == pytest.approx(1.1)
    assert inf_b.transfer_id_timeout == pytest.approx(1.1)

    # Redundant reception from multiple interfaces concurrently.
    for tx_x in (tx_a, tx_b):
        assert await_(
            tx_x.send(
                Transfer(
                    timestamp=Timestamp.now(),
                    priority=Priority.HIGH,
                    transfer_id=2,
                    fragmented_payload=[memoryview(b"def")],
                ),
                loop.time() + 1.0,
            ))
        assert await_(
            tx_x.send(
                Transfer(
                    timestamp=Timestamp.now(),
                    priority=Priority.HIGH,
                    transfer_id=3,
                    fragmented_payload=[memoryview(b"ghi")],
                ),
                loop.time() + 1.0,
            ))

    tr = await_(ses.receive(loop.time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 2
    assert tr.fragmented_payload == [memoryview(b"def")]

    tr = await_(ses.receive(loop.time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 3
    assert tr.fragmented_payload == [memoryview(b"ghi")]

    assert None is await_(
        ses.receive(loop.time() + 2.0))  # Nothing left to read now.

    # This one will be accepted despite a smaller transfer-ID because of the TID timeout.
    assert await_(
        tx_a.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=1,
                fragmented_payload=[memoryview(b"acc")],
            ),
            loop.time() + 1.0,
        ))
    tr = await_(ses.receive(loop.time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 1
    assert tr.fragmented_payload == [memoryview(b"acc")]
    assert tr.inferior_session == inf_a

    # Stats check.
    assert ses.sample_statistics() == RedundantSessionStatistics(
        transfers=3,
        frames=inf_a.sample_statistics().frames +
        inf_b.sample_statistics().frames,
        payload_bytes=9,
        errors=0,
        drops=0,
        inferiors=[
            inf_a.sample_statistics(),
            inf_b.sample_statistics(),
        ],
    )

    ses.close()
Ejemplo n.º 26
0
def _unittest_redundant_input_cyclic() -> None:
    import time
    import pytest
    from pyuavcan.transport import Transfer, Timestamp, Priority, ResourceClosedError
    from pyuavcan.transport.loopback import LoopbackTransport

    loop = asyncio.get_event_loop()
    await_ = loop.run_until_complete

    spec = pyuavcan.transport.InputSessionSpecifier(
        pyuavcan.transport.MessageDataSpecifier(4321), None)
    spec_tx = pyuavcan.transport.OutputSessionSpecifier(
        spec.data_specifier, None)
    meta = pyuavcan.transport.PayloadMetadata(30)

    ts = Timestamp.now()

    tr_a = LoopbackTransport(111)
    tr_b = LoopbackTransport(111)
    tx_a = tr_a.get_output_session(spec_tx, meta)
    tx_b = tr_b.get_output_session(spec_tx, meta)
    inf_a = tr_a.get_input_session(spec, meta)
    inf_b = tr_b.get_input_session(spec, meta)

    inf_a.transfer_id_timeout = 1.1  # This is used to ensure that the transfer-ID timeout is handled correctly.

    is_retired = False

    def retire() -> None:
        nonlocal is_retired
        is_retired = True

    ses = RedundantInputSession(
        spec,
        meta,
        tid_modulo_provider=lambda: 32,
        loop=loop,
        finalizer=retire  # Like CAN, for example.
    )
    assert not is_retired
    assert ses.specifier is spec
    assert ses.payload_metadata is meta
    assert not ses.inferiors
    assert ses.sample_statistics() == RedundantSessionStatistics()
    assert pytest.approx(0.0) == ses.transfer_id_timeout

    # Empty inferior set reception.
    time_before = loop.time()
    assert not await_(ses.receive(loop.time() + 2.0))
    assert 1.0 < loop.time(
    ) - time_before < 5.0, "The method should have returned in about two seconds."

    # Begin reception, then add an inferior while the reception is in progress.
    assert await_(
        tx_a.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=1,
                fragmented_payload=[memoryview(b"abc")],
            ),
            loop.time() + 1.0,
        ))

    async def add_inferior(inferior: pyuavcan.transport.InputSession) -> None:
        await asyncio.sleep(1.0)
        ses._add_inferior(inferior)  # pylint: disable=protected-access

    time_before = loop.time()
    tr, _ = await_(
        asyncio.gather(
            # Start reception here. It would stall for two seconds because no inferiors.
            ses.receive(loop.time() + 2.0),
            # While the transmission is stalled, add one inferior with a delay.
            add_inferior(inf_a),
        ))
    assert 0.0 < loop.time(
    ) - time_before < 5.0, "The method should have returned in about one second."
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 1
    assert tr.fragmented_payload == [memoryview(b"abc")]
    assert tr.inferior_session == inf_a

    # More inferiors
    assert ses.transfer_id_timeout == pytest.approx(1.1)
    ses._add_inferior(inf_a)  # No change, added above    # pylint: disable=protected-access
    assert ses.inferiors == [inf_a]
    ses._add_inferior(inf_b)  # pylint: disable=protected-access
    assert ses.inferiors == [inf_a, inf_b]
    assert ses.transfer_id_timeout == pytest.approx(1.1)
    assert inf_b.transfer_id_timeout == pytest.approx(1.1)

    # Redundant reception - new transfers accepted because the iface switch timeout is exceeded.
    time.sleep(ses.transfer_id_timeout
               )  # Just to make sure that it is REALLY exceeded.
    assert await_(
        tx_b.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=2,
                fragmented_payload=[memoryview(b"def")],
            ),
            loop.time() + 1.0,
        ))
    assert await_(
        tx_b.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=3,
                fragmented_payload=[memoryview(b"ghi")],
            ),
            loop.time() + 1.0,
        ))

    tr = await_(ses.receive(loop.time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 2
    assert tr.fragmented_payload == [memoryview(b"def")]
    assert tr.inferior_session == inf_b

    tr = await_(ses.receive(loop.time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 3
    assert tr.fragmented_payload == [memoryview(b"ghi")]
    assert tr.inferior_session == inf_b

    assert None is await_(
        ses.receive(loop.time() + 1.0))  # Nothing left to read now.

    # This one will be rejected because wrong iface and the switch timeout is not yet exceeded.
    assert await_(
        tx_a.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=4,
                fragmented_payload=[memoryview(b"rej")],
            ),
            loop.time() + 1.0,
        ))
    assert None is await_(ses.receive(loop.time() + 0.1))

    # Transfer-ID timeout reconfiguration.
    ses.transfer_id_timeout = 3.0
    with pytest.raises(ValueError):
        ses.transfer_id_timeout = -0.0
    assert ses.transfer_id_timeout == pytest.approx(3.0)
    assert inf_a.transfer_id_timeout == pytest.approx(3.0)
    assert inf_a.transfer_id_timeout == pytest.approx(3.0)

    # Inferior removal resets the state of the deduplicator.
    ses._close_inferior(0)  # pylint: disable=protected-access
    ses._close_inferior(1)  # Out of range, no effect.  # pylint: disable=protected-access
    assert ses.inferiors == [inf_b]

    assert await_(
        tx_b.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=1,
                fragmented_payload=[memoryview(b"acc")],
            ),
            loop.time() + 1.0,
        ))
    tr = await_(ses.receive(loop.time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 1
    assert tr.fragmented_payload == [memoryview(b"acc")]
    assert tr.inferior_session == inf_b

    # Stats check.
    assert ses.sample_statistics() == RedundantSessionStatistics(
        transfers=4,
        frames=inf_b.sample_statistics().frames,
        payload_bytes=12,
        errors=0,
        drops=0,
        inferiors=[
            inf_b.sample_statistics(),
        ],
    )

    # Closure.
    assert not is_retired
    ses.close()
    assert is_retired
    is_retired = False
    ses.close()
    assert not is_retired
    assert not ses.inferiors
    with pytest.raises(ResourceClosedError):
        await_(ses.receive(0))
Ejemplo n.º 27
0
async def _unittest_redundant_transport(caplog: typing.Any) -> None:
    from pyuavcan.transport import MessageDataSpecifier, PayloadMetadata, Transfer
    from pyuavcan.transport import Priority, Timestamp, InputSessionSpecifier, OutputSessionSpecifier
    from pyuavcan.transport import ProtocolParameters

    loop = asyncio.get_event_loop()
    loop.slow_callback_duration = 1.0

    tr_a = RedundantTransport()
    tr_b = RedundantTransport(loop)
    assert tr_a.sample_statistics() == RedundantTransportStatistics([])
    assert tr_a.inferiors == []
    assert tr_a.local_node_id is None
    assert tr_a.loop is asyncio.get_event_loop()
    assert tr_a.local_node_id is None
    assert tr_a.protocol_parameters == ProtocolParameters(
        transfer_id_modulo=0,
        max_nodes=0,
        mtu=0,
    )
    assert tr_a.descriptor == '<redundant></redundant>'  # Empty, no inferiors.
    assert tr_a.input_sessions == []
    assert tr_a.output_sessions == []

    assert tr_a.loop == tr_b.loop

    #
    # Instantiate session objects.
    #
    meta = PayloadMetadata(10_240)

    pub_a = tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    sub_any_a = tr_a.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    assert pub_a is tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    assert set(tr_a.input_sessions) == {sub_any_a}
    assert set(tr_a.output_sessions) == {pub_a}
    assert tr_a.sample_statistics() == RedundantTransportStatistics()

    pub_b = tr_b.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    sub_any_b = tr_b.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    sub_sel_b = tr_b.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta)
    assert sub_sel_b is tr_b.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta)
    assert set(tr_b.input_sessions) == {sub_any_b, sub_sel_b}
    assert set(tr_b.output_sessions) == {pub_b}
    assert tr_b.sample_statistics() == RedundantTransportStatistics()

    #
    # Exchange test with no inferiors, expected to fail.
    #
    assert len(pub_a.inferiors) == 0
    assert len(sub_any_a.inferiors) == 0
    assert not await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=1,
        fragmented_payload=[memoryview(b'abc')]),
                                      monotonic_deadline=loop.time() + 1.0)
    assert not await sub_any_a.receive_until(loop.time() + 0.1)
    assert not await sub_any_b.receive_until(loop.time() + 0.1)
    assert tr_a.sample_statistics() == RedundantTransportStatistics()
    assert tr_b.sample_statistics() == RedundantTransportStatistics()

    #
    # Adding inferiors - loopback, transport A only.
    #
    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*loop.*'):
        tr_a.attach_inferior(
            LoopbackTransport(
                111, loop=asyncio.new_event_loop()))  # Wrong event loop.
    assert len(pub_a.inferiors) == 0
    assert len(sub_any_a.inferiors) == 0

    lo_mono_0 = LoopbackTransport(111)
    lo_mono_1 = LoopbackTransport(111)

    tr_a.attach_inferior(lo_mono_0)
    assert len(pub_a.inferiors) == 1
    assert len(sub_any_a.inferiors) == 1

    with pytest.raises(ValueError):
        tr_a.detach_inferior(lo_mono_1)  # Not a registered inferior (yet).

    tr_a.attach_inferior(lo_mono_1)
    assert len(pub_a.inferiors) == 2
    assert len(sub_any_a.inferiors) == 2

    with pytest.raises(ValueError):
        tr_a.attach_inferior(lo_mono_0)  # Double-add not allowed.

    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*node-id.*'):
        tr_a.attach_inferior(LoopbackTransport(None))  # Wrong node-ID.

    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*node-id.*'):
        tr_a.attach_inferior(LoopbackTransport(1230))  # Wrong node-ID.

    assert tr_a.inferiors == [lo_mono_0, lo_mono_1]
    assert len(pub_a.inferiors) == 2
    assert len(sub_any_a.inferiors) == 2

    assert tr_a.sample_statistics() == RedundantTransportStatistics(inferiors=[
        lo_mono_0.sample_statistics(),
        lo_mono_1.sample_statistics(),
    ])
    assert tr_a.local_node_id == 111
    assert tr_a.descriptor == '<redundant><loopback/><loopback/></redundant>'

    assert await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=2,
        fragmented_payload=[memoryview(b'def')]),
                                  monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_a.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'def')]
    assert rx.transfer_id == 2
    assert not await sub_any_b.receive_until(loop.time() + 0.1)

    #
    # Incapacitate one inferior, ensure things are still OK.
    #
    with caplog.at_level(logging.CRITICAL,
                         logger=pyuavcan.transport.redundant.__name__):
        for s in lo_mono_0.output_sessions:
            s.exception = RuntimeError('INTENDED EXCEPTION')

        assert await pub_a.send_until(Transfer(
            timestamp=Timestamp.now(),
            priority=Priority.LOW,
            transfer_id=3,
            fragmented_payload=[memoryview(b'qwe')]),
                                      monotonic_deadline=loop.time() + 1.0)
        rx = await sub_any_a.receive_until(loop.time() + 1.0)
        assert rx is not None
        assert rx.fragmented_payload == [memoryview(b'qwe')]
        assert rx.transfer_id == 3

    #
    # Remove old loopback transports. Configure new ones with cyclic TID.
    #
    lo_cyc_0 = LoopbackTransport(111)
    lo_cyc_1 = LoopbackTransport(111)
    cyc_proto_params = ProtocolParameters(
        transfer_id_modulo=32,  # Like CAN
        max_nodes=128,  # Like CAN
        mtu=63,  # Like CAN
    )
    lo_cyc_0.protocol_parameters = cyc_proto_params
    lo_cyc_1.protocol_parameters = cyc_proto_params
    assert lo_cyc_0.protocol_parameters == lo_cyc_1.protocol_parameters == cyc_proto_params

    assert tr_a.protocol_parameters.transfer_id_modulo >= 2**56
    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*transfer-id.*'):
        tr_a.attach_inferior(lo_cyc_0)  # Transfer-ID modulo mismatch

    tr_a.detach_inferior(lo_mono_0)
    tr_a.detach_inferior(lo_mono_1)
    del lo_mono_0  # Prevent accidental reuse.
    del lo_mono_1
    assert tr_a.inferiors == []  # All removed, okay.
    assert pub_a.inferiors == []
    assert sub_any_a.inferiors == []
    assert tr_a.local_node_id is None  # Back to the roots
    assert tr_a.descriptor == '<redundant></redundant>'  # Yes yes

    # Now we can add our cyclic transports safely.
    tr_a.attach_inferior(lo_cyc_0)
    assert tr_a.protocol_parameters.transfer_id_modulo == 32
    tr_a.attach_inferior(lo_cyc_1)
    assert tr_a.protocol_parameters == cyc_proto_params, 'Protocol parameter mismatch'
    assert tr_a.local_node_id == 111
    assert tr_a.descriptor == '<redundant><loopback/><loopback/></redundant>'

    # Exchange test.
    assert await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=4,
        fragmented_payload=[memoryview(b'rty')]),
                                  monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_a.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'rty')]
    assert rx.transfer_id == 4

    #
    # Real heterogeneous transport test.
    #
    tr_a.detach_inferior(lo_cyc_0)
    tr_a.detach_inferior(lo_cyc_1)
    del lo_cyc_0  # Prevent accidental reuse.
    del lo_cyc_1

    udp_a = UDPTransport('127.0.0.111/8')
    udp_b = UDPTransport('127.0.0.222/8')

    serial_a = SerialTransport(SERIAL_URI, 111)
    serial_b = SerialTransport(SERIAL_URI, 222, mtu=2048)  # Heterogeneous.

    tr_a.attach_inferior(udp_a)
    tr_a.attach_inferior(serial_a)

    tr_b.attach_inferior(udp_b)
    tr_b.attach_inferior(serial_b)

    print('tr_a.descriptor', tr_a.descriptor)
    print('tr_b.descriptor', tr_b.descriptor)

    assert tr_a.protocol_parameters == ProtocolParameters(
        transfer_id_modulo=2**64,
        max_nodes=4096,
        mtu=1024,
    )
    assert tr_a.local_node_id == 111
    assert tr_a.descriptor == f'<redundant>{udp_a.descriptor}{serial_a.descriptor}</redundant>'

    assert tr_b.protocol_parameters == ProtocolParameters(
        transfer_id_modulo=2**64,
        max_nodes=4096,
        mtu=1024,
    )
    assert tr_b.local_node_id == 222
    assert tr_b.descriptor == f'<redundant>{udp_b.descriptor}{serial_b.descriptor}</redundant>'

    assert await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=5,
        fragmented_payload=[memoryview(b'uio')]),
                                  monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_b.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'uio')]
    assert rx.transfer_id == 5
    assert not await sub_any_a.receive_until(loop.time() + 0.1)
    assert not await sub_any_b.receive_until(loop.time() + 0.1)
    assert not await sub_sel_b.receive_until(loop.time() + 0.1)

    #
    # Construct new session with the transports configured.
    #
    pub_a_new = tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), 222), meta)
    assert pub_a_new is tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), 222), meta)
    assert set(tr_a.output_sessions) == {pub_a, pub_a_new}

    assert await pub_a_new.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=6,
        fragmented_payload=[memoryview(b'asd')]),
                                      monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_b.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'asd')]
    assert rx.transfer_id == 6

    #
    # Termination.
    #
    tr_a.close()
    tr_a.close()  # Idempotency
    tr_b.close()
    tr_b.close()  # Idempotency

    with pytest.raises(pyuavcan.transport.ResourceClosedError
                       ):  # Make sure the inferiors are closed.
        udp_a.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    with pytest.raises(pyuavcan.transport.ResourceClosedError
                       ):  # Make sure the inferiors are closed.
        serial_b.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    with pytest.raises(pyuavcan.transport.ResourceClosedError
                       ):  # Make sure the sessions are closed.
        await pub_a.send_until(Transfer(timestamp=Timestamp.now(),
                                        priority=Priority.LOW,
                                        transfer_id=100,
                                        fragmented_payload=[]),
                               monotonic_deadline=loop.time() + 1.0)

    await asyncio.sleep(
        1
    )  # Let all pending tasks finalize properly to avoid stack traces in the output.
Ejemplo n.º 28
0
async def _unittest_can_transport_anon() -> None:
    from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer, TransferFrom
    from pyuavcan.transport import UnsupportedSessionConfigurationError, Priority, SessionStatistics, Timestamp
    from pyuavcan.transport import OperationNotDefinedForAnonymousNodeError
    from pyuavcan.transport import InputSessionSpecifier, OutputSessionSpecifier
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._identifier import MessageCANID
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._frame import UAVCANFrame
    from .media.mock import MockMedia, FrameCollector

    with pytest.raises(pyuavcan.transport.InvalidTransportConfigurationError):
        can.CANTransport(MockMedia(set(), 64, 0), None)

    with pytest.raises(pyuavcan.transport.InvalidTransportConfigurationError):
        can.CANTransport(MockMedia(set(), 7, 16), None)

    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, None)
    tr2 = can.CANTransport(media2, None)

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

    assert not media.automatic_retransmission_enabled
    assert not media2.automatic_retransmission_enabled

    assert tr.descriptor == f'<can><mock mtu="64">mock@{id(peers):08x}</mock></can>'

    #
    # Instantiate session objects
    #
    meta = PayloadMetadata(0x_bad_c0ffee_0dd_f00d, 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(12345), None), meta)
    assert broadcaster is tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(12345), 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)
    assert subscriber_selective is tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), 123), meta)

    server_listener = tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            None), meta)
    assert server_listener is 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)
    assert server_responder is 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)
    assert client_requester is 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 client_listener is tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)

    assert broadcaster.destination_node_id is None
    assert subscriber_promiscuous.source_node_id is None
    assert subscriber_selective.source_node_id == 123
    assert server_listener.source_node_id is None
    assert client_listener.source_node_id == 123

    base_ts = time.process_time()
    inputs = tr.input_sessions
    print(
        f'INPUTS (sampled in {time.process_time() - base_ts:.3f}s): {inputs}')
    assert set(inputs) == {
        subscriber_promiscuous, subscriber_selective, server_listener,
        client_listener
    }
    del inputs

    print('OUTPUTS:', tr.output_sessions)
    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_until(
        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().is_same_manifestation(
        UAVCANFrame(
            identifier=MessageCANID(Priority.IMMEDIATE, None, 12345).compile(
                [_mem('abcdef')]),  # payload fragments joined
            padded_payload=_mem('abcdef'),
            transfer_id=11,
            start_of_transfer=True,
            end_of_transfer=True,
            toggle_bit=True,
            loopback=False).compile())
    assert collector.empty

    # Can't send anonymous service transfers
    with pytest.raises(OperationNotDefinedForAnonymousNodeError):
        assert await client_requester.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.IMMEDIATE,
                     transfer_id=0,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0,
        )
    assert client_requester.sample_statistics() == SessionStatistics(
    )  # Not incremented!

    # Can't send multiframe anonymous messages
    with pytest.raises(OperationNotDefinedForAnonymousNodeError):
        assert await broadcaster.send_until(
            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)

    #
    # Broadcast exchange with input dispatch test
    #
    selective_m12345_5 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 5), meta)
    selective_m12345_9 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 9), meta)
    promiscuous_m12345 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), None), meta)

    assert await broadcaster.send_until(
        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_m12345.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 11
    assert received.source_node_id is None  # The sender is anonymous
    assert received.priority == Priority.IMMEDIATE
    validate_timestamp(received.timestamp)
    assert received.fragmented_payload == [_mem('abcdef')]

    assert selective_m12345_5.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert selective_m12345_9.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert promiscuous_m12345.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=6)

    assert not media.automatic_retransmission_enabled
    assert not media2.automatic_retransmission_enabled

    #
    # 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()
Ejemplo n.º 29
0
async def _unittest_loopback_tracer() -> None:
    from pyuavcan.transport import AlienTransfer, AlienSessionSpecifier, AlienTransferMetadata, Timestamp, Priority
    from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, TransferTrace
    from pyuavcan.transport.loopback import LoopbackCapture

    tr = pyuavcan.transport.loopback.LoopbackTransport.make_tracer()
    ts = Timestamp.now()

    # MESSAGE
    msg = AlienTransfer(
        AlienTransferMetadata(
            Priority.IMMEDIATE, 54321,
            AlienSessionSpecifier(1234, None, MessageDataSpecifier(7777))),
        [],
    )
    assert tr.update(LoopbackCapture(ts, msg)) == TransferTrace(
        timestamp=ts,
        transfer=msg,
        transfer_id_timeout=0.0,
    )

    # REQUEST
    req = AlienTransfer(
        AlienTransferMetadata(
            Priority.NOMINAL,
            333333333,
            AlienSessionSpecifier(
                321, 123,
                ServiceDataSpecifier(222, ServiceDataSpecifier.Role.REQUEST)),
        ),
        [],
    )
    trace_req = tr.update(LoopbackCapture(ts, req))
    assert isinstance(trace_req, TransferTrace)
    assert trace_req == TransferTrace(
        timestamp=ts,
        transfer=req,
        transfer_id_timeout=0.0,
    )

    # RESPONSE
    res = AlienTransfer(
        AlienTransferMetadata(
            Priority.NOMINAL,
            333333333,
            AlienSessionSpecifier(
                123, 444,
                ServiceDataSpecifier(222, ServiceDataSpecifier.Role.RESPONSE)),
        ),
        [],
    )
    assert tr.update(LoopbackCapture(ts, res)) == TransferTrace(
        timestamp=ts,
        transfer=res,
        transfer_id_timeout=0.0,
    )

    # RESPONSE
    res = AlienTransfer(
        AlienTransferMetadata(
            Priority.NOMINAL,
            333333333,
            AlienSessionSpecifier(
                123, 321,
                ServiceDataSpecifier(222, ServiceDataSpecifier.Role.RESPONSE)),
        ),
        [],
    )
    assert tr.update(LoopbackCapture(ts, res)) == TransferTrace(
        timestamp=ts,
        transfer=res,
        transfer_id_timeout=0.0,
    )

    # Unknown capture types should yield None.
    assert tr.update(pyuavcan.transport.Capture(ts)) is None
Ejemplo n.º 30
0
async def _unittest_can_transport_non_anon() -> 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
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._identifier import MessageCANID, ServiceCANID
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._frame import UAVCANFrame
    from .media.mock import MockMedia, FrameCollector

    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(0x_bad_c0ffee_0dd_f00d, 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(12345), None), meta)
    assert broadcaster is tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(12345), 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_until(
        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().is_same_manifestation(
        UAVCANFrame(
            identifier=MessageCANID(Priority.IMMEDIATE, 5, 12345).compile(
                [_mem('abcdef')]),  # payload fragments joined
            padded_payload=_mem('abcdef'),
            transfer_id=11,
            start_of_transfer=True,
            end_of_transfer=True,
            toggle_bit=True,
            loopback=False).compile())
    assert collector.empty

    #
    # Broadcast exchange with input dispatch test
    #
    selective_m12345_5 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 5), meta)
    selective_m12345_9 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 9), meta)
    promiscuous_m12345 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), None), meta)

    assert await broadcaster.send_until(
        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_m12345.receive_until(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_m12345_5.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert selective_m12345_9.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert promiscuous_m12345.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_until(
        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_m12345.receive_until(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_until(
        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_m12345.receive_until(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_m12345.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_until(
            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_m12345.receive_until(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_m12345_9.receive_until(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_m12345_5.source_node_id == 5

    received = await selective_m12345_5.receive_until(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_m12345_5.receive_until(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_m12345_5.receive_until(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_m12345_5.sample_statistics(
    ) == promiscuous_m12345.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_until(
        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_until(
        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_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None

    received = await promiscuous_server_s333.receive_until(
        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_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await selective_client_s333_9.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await promiscuous_client_s333.receive_until(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_until(
            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([
        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,
            loopback=True).compile()
    ])

    media.inject_received([
        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,  # Ignored because there is no such transfer-ID in the registry
            loopback=True).compile()
    ])

    # Now, this transmission will succeed, but a pending loopback registry entry will be overwritten, which will be
    # reflected in the error counter.
    assert await client_requester.send_until(
        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([
        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,
                    loopback=True).compile()
    ])

    client_requester.close()
    with pytest.raises(ResourceClosedError):
        assert await client_requester.send_until(
            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_until(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_until(
        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_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None

    # Nothing is received - non-matching role (not server)
    assert (await selective_client_s333_5.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await selective_client_s333_9.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await promiscuous_client_s333.receive_until(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,
            loopback=False)
    ])

    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,
            loopback=False)
    ])

    media.inject_received([
        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,
            loopback=True).compile()
    ])

    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_until(
        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_until(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_until(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!

    assert await pub_m2222.send_until(
        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_until(tr.loop.time() +
                                                          10.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_until(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_until(
        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_until(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_until(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()