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)
示例#2
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
 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 mk_transfer(timestamp:          Timestamp,
                 transfer_id:        int,
                 fragmented_payload: typing.Sequence[typing.Union[bytes, memoryview]]) -> TransferFrom:
     return TransferFrom(timestamp=timestamp,
                         priority=prio,
                         transfer_id=transfer_id,
                         fragmented_payload=list(map(memoryview, fragmented_payload)),
                         source_node_id=src_nid)
 def package(
         fragmented_payload: typing.Sequence[memoryview]) -> TransferFrom:
     return TransferFrom(
         timestamp=timestamp,
         priority=priority,
         transfer_id=transfer_id,
         fragmented_payload=fragmented_payload,
         source_node_id=source_node_id,
     )
示例#6
0
 def trn(
     monotonic_ns: int, transfer_id: int, fragmented_payload: typing.Sequence[typing.Union[bytes, str, memoryview]]
 ) -> TransferFrom:
     return TransferFrom(
         timestamp=Timestamp(system_ns=0, monotonic_ns=monotonic_ns),
         priority=priority,
         transfer_id=transfer_id,
         fragmented_payload=[
             memoryview(x if isinstance(x, (bytes, memoryview)) else x.encode()) for x in fragmented_payload
         ],
         source_node_id=source_node_id,
     )
def _unittest_issue_198() -> None:
    source_node_id = 88
    transfer_id_timeout_ns = 900

    def mk_frame(
        padded_payload: bytes | str,
        transfer_id: int,
        start_of_transfer: bool,
        end_of_transfer: bool,
        toggle_bit: bool,
    ) -> UAVCANFrame:
        return UAVCANFrame(
            identifier=0xBADC0FE,
            padded_payload=memoryview(padded_payload if isinstance(
                padded_payload, bytes) else padded_payload.encode()),
            transfer_id=transfer_id,
            start_of_transfer=start_of_transfer,
            end_of_transfer=end_of_transfer,
            toggle_bit=toggle_bit,
        )

    rx = TransferReassembler(source_node_id, 50)

    # First, ensure that the reassembler is initialized, by feeding it a valid transfer at least once.
    assert rx.process_frame(
        timestamp=Timestamp(system_ns=0, monotonic_ns=1000),
        priority=pyuavcan.transport.Priority.SLOW,
        frame=mk_frame("123", 0, True, True, True),
        transfer_id_timeout_ns=transfer_id_timeout_ns,
    ) == TransferFrom(
        timestamp=Timestamp(system_ns=0, monotonic_ns=1000),
        priority=pyuavcan.transport.Priority.SLOW,
        transfer_id=0,
        fragmented_payload=[
            memoryview(x if isinstance(x, (bytes, memoryview)) else x.encode())
            for x in ["123"]
        ],
        source_node_id=source_node_id,
    )

    # Next, feed the last frame of another transfer whose TID/TOG match the expected state of the reassembler.
    # This should be recognized as a CRC error.
    assert (rx.process_frame(
        timestamp=Timestamp(system_ns=0, monotonic_ns=1000),
        priority=pyuavcan.transport.Priority.SLOW,
        frame=mk_frame("456", 1, False, True, True),
        transfer_id_timeout_ns=transfer_id_timeout_ns,
    ) == TransferReassemblyErrorID.TRANSFER_CRC_MISMATCH)
 def construct_anonymous_transfer(
         timestamp: Timestamp,
         frame: Frame) -> typing.Optional[TransferFrom]:
     """
     A minor helper that validates whether the frame is a valid anonymous transfer (it is if the index
     is zero and the end-of-transfer flag is set) and constructs a transfer instance if it is.
     Otherwise, returns None.
     Observe that this is a static method because anonymous transfers are fundamentally stateless.
     """
     if frame.single_frame_transfer:
         return TransferFrom(
             timestamp=timestamp,
             priority=frame.priority,
             transfer_id=frame.transfer_id,
             fragmented_payload=[frame.payload],
             source_node_id=None,
         )
     return None
    def process_frame(
        self,
        timestamp: Timestamp,
        priority: pyuavcan.transport.Priority,
        frame: UAVCANFrame,
        transfer_id_timeout_ns: int,
    ) -> None | TransferReassemblyErrorID | TransferFrom:
        """
        Observe that occasionally newer frames may have lower timestamp values due to error variations in the time
        recovery algorithms, depending on the methods of timestamping. This class therefore does not check if the
        timestamp values are monotonically increasing. The timestamp of a transfer will be the lowest (earliest)
        timestamp value of its frames (ignoring frames with mismatching transfer ID or toggle bit).
        """
        # FIRST STAGE - DETECTION OF NEW TRANSFERS.
        # Decide if we need to begin a new transfer.
        tid_timed_out = (timestamp.monotonic_ns - self._timestamp.monotonic_ns
                         > transfer_id_timeout_ns
                         or self._timestamp.monotonic_ns == 0)

        not_previous_tid = compute_transfer_id_forward_distance(
            frame.transfer_id, self._transfer_id) > 1

        if tid_timed_out or (frame.start_of_transfer and not_previous_tid):
            self._transfer_id = frame.transfer_id
            self._toggle_bit = frame.toggle_bit
            if not frame.start_of_transfer:
                return TransferReassemblyErrorID.MISSED_START_OF_TRANSFER

        # SECOND STAGE - DROP UNEXPECTED FRAMES.
        # A properly functioning CAN bus may occasionally replicate frames (see the Specification for background).
        # We combat these issues by checking the transfer ID and the toggle bit.
        if frame.transfer_id != self._transfer_id:
            return TransferReassemblyErrorID.UNEXPECTED_TRANSFER_ID

        if frame.toggle_bit != self._toggle_bit:
            return TransferReassemblyErrorID.UNEXPECTED_TOGGLE_BIT

        # THIRD STAGE - PAYLOAD REASSEMBLY AND VERIFICATION.
        # Collect the data and check its correctness.
        if frame.start_of_transfer:
            self._crc = pyuavcan.transport.commons.crc.CRC16CCITT()
            self._payload_truncated = False
            self._fragmented_payload.clear()
            self._timestamp = timestamp  # Initialization from the first frame

        if self._timestamp.monotonic_ns > timestamp.monotonic_ns or self._timestamp.system_ns > timestamp.system_ns:
            # The timestamping algorithm may have corrected the time error since the first frame, accept lower value
            self._timestamp = Timestamp.combine_oldest(self._timestamp,
                                                       timestamp)

        self._toggle_bit = not self._toggle_bit
        # Implicit truncation rule - discard the unexpected data at the end of the payload but compute the CRC anyway.
        self._crc.add(frame.padded_payload)
        if sum(map(len, self._fragmented_payload)
               ) < self._max_payload_size_bytes_with_crc:
            self._fragmented_payload.append(frame.padded_payload)
        else:
            self._payload_truncated = True

        if frame.end_of_transfer:
            fragmented_payload = self._fragmented_payload.copy()
            self._prepare_for_next_transfer()
            self._fragmented_payload.clear()

            if frame.start_of_transfer:
                assert len(
                    fragmented_payload
                ) == 1  # Single-frame transfer, additional checks not needed
            else:
                # Multi-frame transfer, check and remove the trailing CRC.
                # We don't bother checking the CRC if we received fewer than 2 frames because that implies that there
                # was a TID wraparound mid-transfer.
                # This happens when the reassembler that has just been reset is fed with the last frame of another
                # transfer, whose TOGGLE and TRANSFER-ID happen to match the expectations of the reassembler:
                #   1. Wait for the reassembler to be reset. Let: expected transfer-ID = X, expected toggle bit = Y.
                #   2. Construct a frame with SOF=0, EOF=1, TID=X, TOGGLE=Y.
                #   3. Feed the frame into the reassembler.
                # See https://github.com/UAVCAN/pyuavcan/issues/198. There is a dedicated test covering this case.
                if len(fragmented_payload) < 2 or not self._crc.check_residue(
                ):
                    return TransferReassemblyErrorID.TRANSFER_CRC_MISMATCH

                # Cut off the CRC, unless it's already been removed by the implicit payload truncation rule.
                if not self._payload_truncated:
                    expected_length = sum(map(
                        len, fragmented_payload)) - TRANSFER_CRC_LENGTH_BYTES
                    if len(fragmented_payload[-1]) > TRANSFER_CRC_LENGTH_BYTES:
                        fragmented_payload[-1] = fragmented_payload[
                            -1][:-TRANSFER_CRC_LENGTH_BYTES]
                    else:
                        cutoff = TRANSFER_CRC_LENGTH_BYTES - len(
                            fragmented_payload[-1])
                        assert cutoff >= 0
                        fragmented_payload = fragmented_payload[:
                                                                -1]  # Drop the last fragment
                        if cutoff > 0:
                            fragmented_payload[-1] = fragmented_payload[
                                -1][:-cutoff]  # Truncate the previous fragment
                    assert expected_length == sum(map(len, fragmented_payload))

            return TransferFrom(
                timestamp=self._timestamp,
                priority=priority,
                transfer_id=frame.transfer_id,
                fragmented_payload=fragmented_payload,
                source_node_id=self._source_node_id,
            )

        return None  # Expect more frames to come
示例#10
0
def _unittest_input_session() -> None:
    import asyncio
    from pytest import raises, approx
    from pyuavcan.transport import InputSessionSpecifier, MessageDataSpecifier, Priority, TransferFrom
    from pyuavcan.transport import PayloadMetadata, Timestamp
    from pyuavcan.transport.commons.high_overhead_transport import TransferCRC

    ts = Timestamp.now()
    prio = Priority.SLOW
    dst_nid = 1234

    run_until_complete = asyncio.get_event_loop().run_until_complete
    get_monotonic = asyncio.get_event_loop().time

    nihil_supernum = b'nihil supernum'

    finalized = False

    def do_finalize() -> None:
        nonlocal finalized
        finalized = True

    session_spec = InputSessionSpecifier(MessageDataSpecifier(12345), None)
    payload_meta = PayloadMetadata(0xdead_beef_bad_c0ffe, 100)

    sis = SerialInputSession(specifier=session_spec,
                             payload_metadata=payload_meta,
                             loop=asyncio.get_event_loop(),
                             finalizer=do_finalize)
    assert sis.specifier == session_spec
    assert sis.payload_metadata == payload_meta
    assert sis.sample_statistics() == SerialInputSessionStatistics()

    assert sis.transfer_id_timeout == approx(
        SerialInputSession.DEFAULT_TRANSFER_ID_TIMEOUT)
    sis.transfer_id_timeout = 1.0
    with raises(ValueError):
        sis.transfer_id_timeout = 0.0
    assert sis.transfer_id_timeout == approx(1.0)

    assert run_until_complete(sis.receive_until(get_monotonic() + 0.1)) is None
    assert run_until_complete(sis.receive_until(0.0)) is None

    def mk_frame(transfer_id: int, index: int, end_of_transfer: bool,
                 payload: typing.Union[bytes, memoryview],
                 source_node_id: typing.Optional[int]) -> SerialFrame:
        return SerialFrame(timestamp=ts,
                           priority=prio,
                           transfer_id=transfer_id,
                           index=index,
                           end_of_transfer=end_of_transfer,
                           payload=memoryview(payload),
                           source_node_id=source_node_id,
                           destination_node_id=dst_nid,
                           data_specifier=session_spec.data_specifier,
                           data_type_hash=payload_meta.data_type_hash)

    # ANONYMOUS TRANSFERS.
    sis._process_frame(
        mk_frame(transfer_id=0,
                 index=0,
                 end_of_transfer=False,
                 payload=nihil_supernum,
                 source_node_id=None))
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        frames=1,
        errors=1,
    )

    sis._process_frame(
        mk_frame(transfer_id=0,
                 index=1,
                 end_of_transfer=True,
                 payload=nihil_supernum,
                 source_node_id=None))
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        frames=2,
        errors=2,
    )

    sis._process_frame(
        mk_frame(transfer_id=0,
                 index=0,
                 end_of_transfer=True,
                 payload=nihil_supernum,
                 source_node_id=None))
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        transfers=1,
        frames=3,
        payload_bytes=len(nihil_supernum),
        errors=2,
    )
    assert run_until_complete(sis.receive_until(0)) == \
        TransferFrom(timestamp=ts,
                     priority=prio,
                     transfer_id=0,
                     fragmented_payload=[memoryview(nihil_supernum)],
                     source_node_id=None)
    assert run_until_complete(sis.receive_until(get_monotonic() + 0.1)) is None
    assert run_until_complete(sis.receive_until(0.0)) is None

    # BAD DATA TYPE HASH.
    sis._process_frame(
        SerialFrame(timestamp=ts,
                    priority=prio,
                    transfer_id=0,
                    index=0,
                    end_of_transfer=True,
                    payload=memoryview(nihil_supernum),
                    source_node_id=None,
                    destination_node_id=None,
                    data_specifier=session_spec.data_specifier,
                    data_type_hash=0xbad_bad_bad_bad_bad))
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        transfers=1,
        frames=4,
        payload_bytes=len(nihil_supernum),
        errors=3,
        mismatched_data_type_hashes={0xbad_bad_bad_bad_bad: 1},
    )
示例#11
0
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        transfers=3,
        frames=9,
        payload_bytes=len(nihil_supernum) * 5,
        errors=3,
        mismatched_data_type_hashes={0xbad_bad_bad_bad_bad: 1},
        reassembly_errors_per_source_node_id={
            1111: {},
            2222: {},
        },
    )

    assert run_until_complete(sis.receive_until(0)) == \
        TransferFrom(timestamp=ts,
                     priority=prio,
                     transfer_id=0,
                     fragmented_payload=[memoryview(nihil_supernum)],
                     source_node_id=2222)
    assert run_until_complete(sis.receive_until(0)) == \
        TransferFrom(timestamp=ts,
                     priority=prio,
                     transfer_id=0,
                     fragmented_payload=[memoryview(nihil_supernum)] * 3,
                     source_node_id=1111)
    assert run_until_complete(sis.receive_until(get_monotonic() + 0.1)) is None
    assert run_until_complete(sis.receive_until(0.0)) is None

    # TRANSFERS WITH REASSEMBLY ERRORS.
    sis._process_frame(
        mk_frame(
            transfer_id=1,  # EMPTY IN MULTIFRAME
示例#12
0
async def _unittest_input_session() -> None:
    ts = Timestamp.now()
    prio = Priority.SLOW
    dst_nid = 1234

    get_monotonic = asyncio.get_event_loop().time

    nihil_supernum = b"nihil supernum"

    finalized = False

    def do_finalize() -> None:
        nonlocal finalized
        finalized = True

    session_spec = InputSessionSpecifier(MessageDataSpecifier(2345), None)
    payload_meta = PayloadMetadata(100)

    sis = SerialInputSession(specifier=session_spec,
                             payload_metadata=payload_meta,
                             finalizer=do_finalize)
    assert sis.specifier == session_spec
    assert sis.payload_metadata == payload_meta
    assert sis.sample_statistics() == SerialInputSessionStatistics()

    assert sis.transfer_id_timeout == approx(
        SerialInputSession.DEFAULT_TRANSFER_ID_TIMEOUT)
    sis.transfer_id_timeout = 1.0
    with raises(ValueError):
        sis.transfer_id_timeout = 0.0
    assert sis.transfer_id_timeout == approx(1.0)

    assert await (sis.receive(get_monotonic() + 0.1)) is None
    assert await (sis.receive(0.0)) is None

    def mk_frame(
        transfer_id: int,
        index: int,
        end_of_transfer: bool,
        payload: typing.Union[bytes, memoryview],
        source_node_id: typing.Optional[int],
    ) -> SerialFrame:
        return SerialFrame(
            priority=prio,
            transfer_id=transfer_id,
            index=index,
            end_of_transfer=end_of_transfer,
            payload=memoryview(payload),
            source_node_id=source_node_id,
            destination_node_id=dst_nid,
            data_specifier=session_spec.data_specifier,
        )

    # ANONYMOUS TRANSFERS.
    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(transfer_id=0,
                 index=0,
                 end_of_transfer=False,
                 payload=nihil_supernum,
                 source_node_id=None))
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        frames=1,
        errors=1,
    )

    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(transfer_id=0,
                 index=1,
                 end_of_transfer=True,
                 payload=nihil_supernum,
                 source_node_id=None))
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        frames=2,
        errors=2,
    )

    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(transfer_id=0,
                 index=0,
                 end_of_transfer=True,
                 payload=nihil_supernum,
                 source_node_id=None))
    assert sis.sample_statistics() == SerialInputSessionStatistics(
        transfers=1,
        frames=3,
        payload_bytes=len(nihil_supernum),
        errors=2,
    )
    assert await (sis.receive(0)) == TransferFrom(
        timestamp=ts,
        priority=prio,
        transfer_id=0,
        fragmented_payload=[memoryview(nihil_supernum)],
        source_node_id=None)
    assert await (sis.receive(get_monotonic() + 0.1)) is None
    assert await (sis.receive(0.0)) is None

    # VALID TRANSFERS. Notice that they are unordered on purpose. The reassembler can deal with that.
    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(transfer_id=0,
                 index=1,
                 end_of_transfer=False,
                 payload=nihil_supernum,
                 source_node_id=1111))

    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(transfer_id=0,
                 index=0,
                 end_of_transfer=True,
                 payload=nihil_supernum,
                 source_node_id=2222))  # COMPLETED FIRST

    assert sis.sample_statistics() == SerialInputSessionStatistics(
        transfers=2,
        frames=5,
        payload_bytes=len(nihil_supernum) * 2,
        errors=2,
        reassembly_errors_per_source_node_id={
            1111: {},
            2222: {},
        },
    )

    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(
            transfer_id=0,
            index=3,
            end_of_transfer=True,
            payload=TransferCRC.new(nihil_supernum * 3).value_as_bytes,
            source_node_id=1111,
        ),
    )

    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(transfer_id=0,
                 index=0,
                 end_of_transfer=False,
                 payload=nihil_supernum,
                 source_node_id=1111))

    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(transfer_id=0,
                 index=2,
                 end_of_transfer=False,
                 payload=nihil_supernum,
                 source_node_id=1111))  # COMPLETED SECOND

    assert sis.sample_statistics() == SerialInputSessionStatistics(
        transfers=3,
        frames=8,
        payload_bytes=len(nihil_supernum) * 5,
        errors=2,
        reassembly_errors_per_source_node_id={
            1111: {},
            2222: {},
        },
    )

    assert await (sis.receive(0)) == TransferFrom(
        timestamp=ts,
        priority=prio,
        transfer_id=0,
        fragmented_payload=[memoryview(nihil_supernum)],
        source_node_id=2222)
    assert await (sis.receive(0)) == TransferFrom(
        timestamp=ts,
        priority=prio,
        transfer_id=0,
        fragmented_payload=[memoryview(nihil_supernum)] * 3,
        source_node_id=1111,
    )
    assert await (sis.receive(get_monotonic() + 0.1)) is None
    assert await (sis.receive(0.0)) is None

    # TRANSFERS WITH REASSEMBLY ERRORS.
    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(
            transfer_id=1,
            index=0,
            end_of_transfer=False,
            payload=b"",
            source_node_id=1111  # EMPTY IN MULTIFRAME
        ),
    )

    sis._process_frame(  # pylint: disable=protected-access
        ts,
        mk_frame(
            transfer_id=2,
            index=0,
            end_of_transfer=False,
            payload=b"",
            source_node_id=1111  # EMPTY IN MULTIFRAME
        ),
    )

    assert sis.sample_statistics() == SerialInputSessionStatistics(
        transfers=3,
        frames=10,
        payload_bytes=len(nihil_supernum) * 5,
        errors=4,
        reassembly_errors_per_source_node_id={
            1111: {
                TransferReassembler.Error.MULTIFRAME_EMPTY_FRAME: 2,
            },
            2222: {},
        },
    )

    assert not finalized
    sis.close()
    assert finalized
    sis.close()  # Idempotency check
    def process_frame(
            self, timestamp: Timestamp, frame: Frame,
            transfer_id_timeout: float) -> typing.Optional[TransferFrom]:
        """
        Updates the transfer reassembly state machine with the new frame.

        :param timestamp: The reception timestamp from the transport layer.
        :param frame: The new frame.
        :param transfer_id_timeout: The current value of the transfer-ID timeout.
        :return: A new transfer if the new frame completed one. None if the new frame did not complete a transfer.
        :raises: Nothing.
        """
        # DROP MALFORMED FRAMES. A multi-frame transfer cannot contain frames with no payload.
        if not (frame.index == 0
                and frame.end_of_transfer) and not frame.payload:
            self._on_error_callback(self.Error.MULTIFRAME_EMPTY_FRAME)
            return None

        # DETECT NEW TRANSFERS. Either a newer TID or TID-timeout is reached.
        if (frame.transfer_id > self._transfer_id
                or timestamp.monotonic - self._timestamp.monotonic >
                transfer_id_timeout):
            self._restart(
                timestamp, frame.transfer_id,
                self.Error.MULTIFRAME_MISSING_FRAMES
                if self._payloads else None)

        # DROP FRAMES FROM NON-MATCHING TRANSFERS. E.g., duplicates. This is not an error.
        if frame.transfer_id < self._transfer_id:
            return None
        assert frame.transfer_id == self._transfer_id

        # DETERMINE MAX FRAME INDEX FOR THIS TRANSFER. Frame N with EOT, then frame M with EOT, where N != M.
        if frame.end_of_transfer:
            if self._max_index is not None and self._max_index != frame.index:
                self._restart(timestamp, frame.transfer_id + 1,
                              self.Error.MULTIFRAME_EOT_INCONSISTENT)
                return None
            assert self._max_index is None or self._max_index == frame.index
            self._max_index = frame.index

        # DETECT UNEXPECTED FRAMES PAST THE END OF TRANSFER. If EOT is set on index N, then indexes > N are invalid.
        if self._max_index is not None and max(
                frame.index,
                len(self._payloads) - 1) > self._max_index:
            self._restart(timestamp, frame.transfer_id + 1,
                          self.Error.MULTIFRAME_EOT_MISPLACED)
            return None

        # ACCEPT THE PAYLOAD. Duplicates are accepted too, assuming they carry the same payload.
        # Implicit truncation is implemented by not limiting the maximum payload size.
        # Real truncation is hard to implement if frames are delivered out-of-order, although it's not impossible:
        # instead of storing actual payload fragments above the limit, we can store their CRCs.
        # When the last fragment is received, CRC of all fragments are then combined to validate the final transfer-CRC.
        # This method, however, requires knowledge of the MTU to determine which fragments will be above the limit.
        while len(self._payloads) <= frame.index:
            self._payloads.append(memoryview(b""))
        self._payloads[frame.index] = frame.payload

        # CHECK IF ALL FRAMES ARE RECEIVED. If not, simply wait for next frame.
        # Single-frame transfers with empty payload are legal.
        if self._max_index is None or (self._max_index > 0
                                       and not all(self._payloads)):
            return None
        assert self._max_index is not None
        assert self._max_index == len(self._payloads) - 1
        assert all(self._payloads) if self._max_index > 0 else True

        # FINALIZE THE TRANSFER. All frames are received here.
        result = _validate_and_finalize_transfer(
            timestamp=self._timestamp,
            priority=frame.priority,
            transfer_id=frame.transfer_id,
            frame_payloads=self._payloads,
            source_node_id=self._source_node_id,
        )
        self._restart(
            timestamp, frame.transfer_id + 1,
            self.Error.MULTIFRAME_INTEGRITY_ERROR if result is None else None)
        if result is not None:
            # Late implicit truncation. Normally, it should be done on-the-fly, by not storing payload fragments
            # above the maximum expected size, but it is hard to combine with out-of-order frame acceptance.
            while result.fragmented_payload and sum(
                    map(len,
                        result.fragmented_payload[:-1])) > self._extent_bytes:
                # TODO: a minor refactoring is needed to avoid re-creating the transfer instance here.
                result = TransferFrom(
                    timestamp=result.timestamp,
                    priority=result.priority,
                    transfer_id=result.transfer_id,
                    fragmented_payload=result.fragmented_payload[:-1],
                    source_node_id=result.source_node_id,
                )
        return result