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()
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)
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)
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
def _read_frame(self, ts_mono_ns: int) -> typing.Tuple[Timestamp, Envelope]: while True: data, ancdata, msg_flags, _addr = self._sock.recvmsg( self._native_frame_size, self._ancillary_data_buffer_size ) assert msg_flags & socket.MSG_TRUNC == 0, "The data buffer is not large enough" assert msg_flags & socket.MSG_CTRUNC == 0, "The ancillary data buffer is not large enough" loopback = bool(msg_flags & socket.MSG_CONFIRM) ts_system_ns = 0 for cmsg_level, cmsg_type, cmsg_data in ancdata: if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMP: sec, usec = _TIMEVAL_STRUCT.unpack(cmsg_data) ts_system_ns = (sec * 1_000_000 + usec) * 1000 else: assert False, f"Unexpected ancillary data: {cmsg_level}, {cmsg_type}, {cmsg_data!r}" assert ts_system_ns > 0, "Missing the timestamp; does the driver support timestamping?" timestamp = Timestamp(system_ns=ts_system_ns, monotonic_ns=ts_mono_ns) out = SocketCANMedia._parse_native_frame(data) if out is not None: return timestamp, Envelope(out, loopback=loopback)
async def _unittest_can_pythoncan() -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 media_a = PythonCANMedia("virtual:0", 500000) media_b = PythonCANMedia("virtual:0", 500000) assert media_a.mtu == 8 assert media_b.mtu == 8 assert media_a.interface_name == "virtual:0" assert media_b.interface_name == "virtual:0" assert media_a.number_of_acceptance_filters == media_b.number_of_acceptance_filters assert media_a._maybe_thread is None assert media_b._maybe_thread is None rx_a: typing.List[typing.Tuple[Timestamp, Envelope]] = [] rx_b: typing.List[typing.Tuple[Timestamp, Envelope]] = [] def on_rx_a(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None: nonlocal rx_a frames = list(frames) print("RX A:", frames) rx_a += frames def on_rx_b(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None: nonlocal rx_b frames = list(frames) print("RX B:", frames) rx_b += frames media_a.start(on_rx_a, False) media_b.start(on_rx_b, False) assert media_a._maybe_thread is not None assert media_b._maybe_thread is not None await asyncio.sleep(2.0) # This wait is needed to ensure that the RX thread handles read timeout properly ts_begin = Timestamp.now() await media_b.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 0xBADC0FE, bytearray(range(8))), loopback=True), Envelope(DataFrame(FrameFormat.EXTENDED, 0x12345678, bytearray(range(0))), loopback=False), Envelope(DataFrame(FrameFormat.BASE, 0x123, bytearray(range(6))), loopback=True), ], asyncio.get_event_loop().time() + 1.0, ) await asyncio.sleep(0.1) ts_end = Timestamp.now() print("rx_a:", rx_a) # Three received from another part assert len(rx_a) == 3 for ts, _f in rx_a: assert ts_begin.monotonic_ns <= ts.monotonic_ns <= ts_end.monotonic_ns assert ts_begin.system_ns <= ts.system_ns <= ts_end.system_ns rx_external = list(filter(lambda x: True, rx_a)) assert rx_external[0][1].frame.identifier == 0xBADC0FE assert rx_external[0][1].frame.data == bytearray(range(8)) assert rx_external[0][1].frame.format == FrameFormat.EXTENDED assert rx_external[1][1].frame.identifier == 0x12345678 assert rx_external[1][1].frame.data == bytearray(range(0)) assert rx_external[1][1].frame.format == FrameFormat.EXTENDED assert rx_external[2][1].frame.identifier == 0x123 assert rx_external[2][1].frame.data == bytearray(range(6)) assert rx_external[2][1].frame.format == FrameFormat.BASE print("rx_b:", rx_b) # Two messages are loopback and were copied assert len(rx_b) == 2 rx_loopback = list(filter(lambda x: True, rx_b)) assert rx_loopback[0][1].frame.identifier == 0xBADC0FE assert rx_loopback[0][1].frame.data == bytearray(range(8)) assert rx_loopback[0][1].frame.format == FrameFormat.EXTENDED assert rx_loopback[1][1].frame.identifier == 0x123 assert rx_loopback[1][1].frame.data == bytearray(range(6)) assert rx_loopback[1][1].frame.format == FrameFormat.BASE media_a.close() media_b.close()
async def _unittest_can_mock_media() -> None: peers: typing.Set[MockMedia] = set() me = MockMedia(peers, 64, 3) assert len(peers) == 1 and me in peers assert me.mtu == 64 assert me.number_of_acceptance_filters == 3 assert not me.automatic_retransmission_enabled assert str(me) == f"MockMedia('mock@{id(peers):08x}', mtu=64)" me_collector = FrameCollector() me.start(me_collector.give, False) assert me.automatic_retransmission_enabled # Will drop the loopback because of the acceptance filters await me.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")), loopback=False), Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")), loopback=True), ], asyncio.get_event_loop().time() + 1.0, ) assert me_collector.empty me.configure_acceptance_filters([FilterConfiguration.new_promiscuous()]) # Now the loopback will be accepted because we have reconfigured the filters await me.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")), loopback=False), Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")), loopback=True), ], asyncio.get_event_loop().time() + 1.0, ) assert me_collector.pop()[1].frame == DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")) assert me_collector.empty pe = MockMedia(peers, 8, 1) assert peers == {me, pe} pe_collector = FrameCollector() pe.start(pe_collector.give, False) me.raise_on_send_once(RuntimeError("Hello world!")) with pytest.raises(RuntimeError, match="Hello world!"): await me.send([], asyncio.get_event_loop().time() + 1.0) await me.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")), loopback=False), Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")), loopback=True), ], asyncio.get_event_loop().time() + 1.0, ) assert pe_collector.empty pe.configure_acceptance_filters([FilterConfiguration(123, 127, None)]) await me.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")), loopback=False), Envelope(DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")), loopback=True), ], asyncio.get_event_loop().time() + 1.0, ) await me.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 456, bytearray(b"ghi")), loopback=False), # Dropped by the filters ], asyncio.get_event_loop().time() + 1.0, ) assert pe_collector.pop()[1].frame == DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"abc")) assert pe_collector.pop()[1].frame == DataFrame(FrameFormat.EXTENDED, 123, bytearray(b"def")) assert pe_collector.empty me.close() me.close() # Idempotency. assert peers == {pe} with pytest.raises(pyuavcan.transport.ResourceClosedError): await me.send([], asyncio.get_event_loop().time() + 1.0) with pytest.raises(pyuavcan.transport.ResourceClosedError): me.configure_acceptance_filters([]) await asyncio.sleep( 1 ) # Let all pending tasks finalize properly to avoid stack traces in the output.
async def _unittest_can_socketcan() -> None: from pyuavcan.transport import Timestamp from pyuavcan.transport.can.media import Envelope, DataFrame, FrameFormat, FilterConfiguration from pyuavcan.transport.can.media.socketcan import SocketCANMedia available = SocketCANMedia.list_available_interface_names() print("Available SocketCAN ifaces:", available) assert "vcan0" in available, ( "Either the interface listing method is not working or the environment is not configured correctly. " 'Please ensure that the virtual SocketCAN interface "vcan0" is available, and its MTU is set to 64+8.' ) media_a = SocketCANMedia("vcan0", 12) media_b = SocketCANMedia("vcan0", 64) assert media_a.mtu == 12 assert media_b.mtu == 64 assert media_a.interface_name == "vcan0" assert media_b.interface_name == "vcan0" assert media_a.number_of_acceptance_filters == media_b.number_of_acceptance_filters assert media_a._maybe_thread is None # pylint: disable=protected-access assert media_b._maybe_thread is None # pylint: disable=protected-access media_a.configure_acceptance_filters( [FilterConfiguration.new_promiscuous()]) media_b.configure_acceptance_filters( [FilterConfiguration.new_promiscuous()]) rx_a: typing.List[typing.Tuple[Timestamp, Envelope]] = [] def on_rx_a( frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None: nonlocal rx_a frames = list(frames) print("RX A:", frames) rx_a += frames def on_rx_b( frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None: frames = list(frames) print("RX B:", frames) asyncio.ensure_future( media_b.send((e for _, e in frames), asyncio.get_event_loop().time() + 1.0)) media_a.start(on_rx_a, False) media_b.start(on_rx_b, True) assert media_a._maybe_thread is not None # pylint: disable=protected-access assert media_b._maybe_thread is not None # pylint: disable=protected-access await asyncio.sleep( 2.0 ) # This wait is needed to ensure that the RX thread handles select() timeout properly ts_begin = Timestamp.now() await media_a.send( [ Envelope(DataFrame(FrameFormat.BASE, 0x123, bytearray(range(6))), loopback=True), Envelope(DataFrame(FrameFormat.EXTENDED, 0x1BADC0FE, bytearray(range(8))), loopback=True), ], asyncio.get_event_loop().time() + 1.0, ) await media_a.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 0x1FF45678, bytearray(range(0))), loopback=False), ], asyncio.get_event_loop().time() + 1.0, ) await asyncio.sleep(1.0) ts_end = Timestamp.now() print("rx_a:", rx_a) # Three sent back from the other end, two loopback assert len(rx_a) == 5 for t, _ in rx_a: assert ts_begin.monotonic_ns <= t.monotonic_ns <= ts_end.monotonic_ns assert ts_begin.system_ns <= t.system_ns <= ts_end.system_ns rx_loopback = [e.frame for t, e in rx_a if e.loopback] rx_external = [e.frame for t, e in rx_a if not e.loopback] assert len(rx_loopback) == 2 and len(rx_external) == 3 assert rx_loopback[0].identifier == 0x123 assert rx_loopback[0].data == bytearray(range(6)) assert rx_loopback[0].format == FrameFormat.BASE assert rx_loopback[1].identifier == 0x1BADC0FE assert rx_loopback[1].data == bytearray(range(8)) assert rx_loopback[1].format == FrameFormat.EXTENDED assert rx_external[0].identifier == 0x123 assert rx_external[0].data == bytearray(range(6)) assert rx_external[0].format == FrameFormat.BASE assert rx_external[1].identifier == 0x1BADC0FE assert rx_external[1].data == bytearray(range(8)) assert rx_external[1].format == FrameFormat.EXTENDED assert rx_external[2].identifier == 0x1FF45678 assert rx_external[2].data == bytearray(range(0)) assert rx_external[2].format == FrameFormat.EXTENDED media_a.close() media_b.close() await asyncio.sleep( 1 ) # Let all pending tasks finalize properly to avoid stack traces in the output.
async def _unittest_can_transport_non_anon(caplog: typing.Any) -> None: from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer, TransferFrom from pyuavcan.transport import UnsupportedSessionConfigurationError, Priority, SessionStatistics, Timestamp from pyuavcan.transport import ResourceClosedError, InputSessionSpecifier, OutputSessionSpecifier from pyuavcan.transport.can._identifier import MessageCANID, ServiceCANID from pyuavcan.transport.can._frame import UAVCANFrame from pyuavcan.transport.can.media import Envelope from .media.mock import MockMedia, FrameCollector asyncio.get_running_loop().slow_callback_duration = 5.0 peers: typing.Set[MockMedia] = set() media = MockMedia(peers, 64, 10) media2 = MockMedia(peers, 64, 3) peeper = MockMedia(peers, 64, 10) assert len(peers) == 3 tr = can.CANTransport(media, 5) tr2 = can.CANTransport(media2, 123) assert tr.protocol_parameters == pyuavcan.transport.ProtocolParameters( transfer_id_modulo=32, max_nodes=128, mtu=63) assert tr.local_node_id == 5 assert tr.protocol_parameters == tr2.protocol_parameters assert media.automatic_retransmission_enabled assert media2.automatic_retransmission_enabled # # Instantiate session objects # meta = PayloadMetadata(10000) with pytest.raises(Exception): # Can't broadcast service calls tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE), None), meta) with pytest.raises( UnsupportedSessionConfigurationError): # Can't unicast messages tr.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(1234), 123), meta) broadcaster = tr.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) assert broadcaster is tr.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) subscriber_promiscuous = tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2222), None), meta) assert subscriber_promiscuous is tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2222), None), meta) subscriber_selective = tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2222), 123), meta) server_listener = tr.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), None), meta) server_responder = tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 123), meta) client_requester = tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 123), meta) client_listener = tr.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 123), meta) assert set(tr.input_sessions) == { subscriber_promiscuous, subscriber_selective, server_listener, client_listener } assert set(tr.output_sessions) == { broadcaster, server_responder, client_requester } # # Basic exchange test, no one is listening # media2.configure_acceptance_filters( [can.media.FilterConfiguration.new_promiscuous()]) peeper.configure_acceptance_filters( [can.media.FilterConfiguration.new_promiscuous()]) collector = FrameCollector() peeper.start(collector.give, False) assert tr.sample_statistics() == can.CANTransportStatistics() assert tr2.sample_statistics() == can.CANTransportStatistics() ts = Timestamp.now() def validate_timestamp(timestamp: Timestamp) -> None: assert ts.monotonic_ns <= timestamp.monotonic_ns <= time.monotonic_ns() assert ts.system_ns <= timestamp.system_ns <= time.time_ns() assert await broadcaster.send( Transfer( timestamp=ts, priority=Priority.IMMEDIATE, transfer_id=32 + 11, # Modulus 11 fragmented_payload=[_mem("abc"), _mem("def")], ), tr.loop.time() + 1.0, ) assert broadcaster.sample_statistics() == SessionStatistics( transfers=1, frames=1, payload_bytes=6) assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=1) assert tr2.sample_statistics() == can.CANTransportStatistics( in_frames=1, in_frames_uavcan=1) assert tr.sample_statistics( ).media_acceptance_filtering_efficiency == pytest.approx(1) assert tr2.sample_statistics( ).media_acceptance_filtering_efficiency == pytest.approx(0) assert tr.sample_statistics().lost_loopback_frames == 0 assert tr2.sample_statistics().lost_loopback_frames == 0 assert (collector.pop()[1].frame == UAVCANFrame( identifier=MessageCANID(Priority.IMMEDIATE, 5, 2345).compile([_mem("abcdef") ]), # payload fragments joined padded_payload=_mem("abcdef"), transfer_id=11, start_of_transfer=True, end_of_transfer=True, toggle_bit=True, ).compile()) assert collector.empty # # Broadcast exchange with input dispatch test # selective_m2345_5 = tr2.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), 5), meta) selective_m2345_9 = tr2.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), 9), meta) promiscuous_m2345 = tr2.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), None), meta) assert await broadcaster.send( Transfer( timestamp=ts, priority=Priority.IMMEDIATE, transfer_id=32 + 11, # Modulus 11 fragmented_payload=[_mem("abc"), _mem("def")], ), tr.loop.time() + 1.0, ) assert broadcaster.sample_statistics() == SessionStatistics( transfers=2, frames=2, payload_bytes=12) assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=2) assert tr2.sample_statistics() == can.CANTransportStatistics( in_frames=2, in_frames_uavcan=2, in_frames_uavcan_accepted=1) received = await promiscuous_m2345.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.transfer_id == 11 assert received.source_node_id == 5 assert received.priority == Priority.IMMEDIATE validate_timestamp(received.timestamp) assert received.fragmented_payload == [_mem("abcdef")] assert selective_m2345_5.sample_statistics() == SessionStatistics( ) # Nothing assert selective_m2345_9.sample_statistics() == SessionStatistics( ) # Nothing assert promiscuous_m2345.sample_statistics() == SessionStatistics( transfers=1, frames=1, payload_bytes=6) assert media.automatic_retransmission_enabled assert media2.automatic_retransmission_enabled feedback_collector = _FeedbackCollector() broadcaster.enable_feedback(feedback_collector.give) assert await broadcaster.send( Transfer( timestamp=ts, priority=Priority.SLOW, transfer_id=2, fragmented_payload=[_mem("qwe"), _mem("rty")] * 50, # Lots of data here, very multiframe ), tr.loop.time() + 1.0, ) assert broadcaster.sample_statistics() == SessionStatistics( transfers=3, frames=7, payload_bytes=312) broadcaster.disable_feedback() assert tr.sample_statistics() == can.CANTransportStatistics( out_frames=7, out_frames_loopback=1, in_frames_loopback=1) assert tr2.sample_statistics() == can.CANTransportStatistics( in_frames=7, in_frames_uavcan=7, in_frames_uavcan_accepted=6) fb = feedback_collector.take() assert fb.original_transfer_timestamp == ts validate_timestamp(fb.first_frame_transmission_timestamp) received = await promiscuous_m2345.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.transfer_id == 2 assert received.source_node_id == 5 assert received.priority == Priority.SLOW validate_timestamp(received.timestamp) assert b"".join( received.fragmented_payload ) == b"qwerty" * 50 + b"\x00" * 13 # The 0x00 at the end is padding assert await broadcaster.send( Transfer(timestamp=ts, priority=Priority.OPTIONAL, transfer_id=3, fragmented_payload=[_mem("qwe"), _mem("rty")]), tr.loop.time() + 1.0, ) assert broadcaster.sample_statistics() == SessionStatistics( transfers=4, frames=8, payload_bytes=318) received = await promiscuous_m2345.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.transfer_id == 3 assert received.source_node_id == 5 assert received.priority == Priority.OPTIONAL validate_timestamp(received.timestamp) assert list(received.fragmented_payload) == [_mem("qwerty")] assert promiscuous_m2345.sample_statistics() == SessionStatistics( transfers=3, frames=7, payload_bytes=325) assert tr.sample_statistics() == can.CANTransportStatistics( out_frames=8, out_frames_loopback=1, in_frames_loopback=1) assert tr2.sample_statistics() == can.CANTransportStatistics( in_frames=8, in_frames_uavcan=8, in_frames_uavcan_accepted=7) broadcaster.close() with pytest.raises(ResourceClosedError): assert await broadcaster.send( Transfer(timestamp=ts, priority=Priority.LOW, transfer_id=4, fragmented_payload=[]), tr.loop.time() + 1.0) broadcaster.close() # Does nothing # Final checks for the broadcaster - make sure nothing is left in the queue assert (await promiscuous_m2345.receive(tr.loop.time() + _RX_TIMEOUT)) is None # The selective listener was not supposed to pick up anything because it's selective for node 9, not 5 assert (await selective_m2345_9.receive(tr.loop.time() + _RX_TIMEOUT)) is None # Now, there are a bunch of items awaiting in the selective input for node 5, collect them and check the stats assert selective_m2345_5.source_node_id == 5 received = await selective_m2345_5.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.transfer_id == 11 assert received.source_node_id == 5 assert received.priority == Priority.IMMEDIATE validate_timestamp(received.timestamp) assert received.fragmented_payload == [_mem("abcdef")] received = await selective_m2345_5.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.transfer_id == 2 assert received.source_node_id == 5 assert received.priority == Priority.SLOW validate_timestamp(received.timestamp) assert b"".join( received.fragmented_payload ) == b"qwerty" * 50 + b"\x00" * 13 # The 0x00 at the end is padding received = await selective_m2345_5.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.transfer_id == 3 assert received.source_node_id == 5 assert received.priority == Priority.OPTIONAL validate_timestamp(received.timestamp) assert list(received.fragmented_payload) == [_mem("qwerty")] assert selective_m2345_5.sample_statistics( ) == promiscuous_m2345.sample_statistics() # # Unicast exchange test # selective_server_s333_5 = tr2.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 5), meta) selective_server_s333_9 = tr2.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 9), meta) promiscuous_server_s333 = tr2.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), None), meta) selective_client_s333_5 = tr2.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 5), meta) selective_client_s333_9 = tr2.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 9), meta) promiscuous_client_s333 = tr2.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), None), meta) assert await client_requester.send( Transfer(timestamp=ts, priority=Priority.FAST, transfer_id=11, fragmented_payload=[]), tr.loop.time() + 1.0) assert client_requester.sample_statistics() == SessionStatistics( transfers=1, frames=1, payload_bytes=0) received = await selective_server_s333_5.receive(tr.loop.time() + 1.0 ) # Same thing here assert received is not None assert received.transfer_id == 11 assert received.priority == Priority.FAST validate_timestamp(received.timestamp) assert list(map(bytes, received.fragmented_payload)) == [b""] assert (await selective_server_s333_9.receive(tr.loop.time() + _RX_TIMEOUT)) is None received = await promiscuous_server_s333.receive(tr.loop.time() + 1.0 ) # Same thing here assert received is not None assert received.transfer_id == 11 assert received.priority == Priority.FAST validate_timestamp(received.timestamp) assert list(map(bytes, received.fragmented_payload)) == [b""] assert selective_server_s333_5.sample_statistics() == SessionStatistics( transfers=1, frames=1) assert selective_server_s333_9.sample_statistics() == SessionStatistics() assert promiscuous_server_s333.sample_statistics() == SessionStatistics( transfers=1, frames=1) assert (await selective_client_s333_5.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert (await selective_client_s333_9.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert (await promiscuous_client_s333.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert selective_client_s333_5.sample_statistics() == SessionStatistics() assert selective_client_s333_9.sample_statistics() == SessionStatistics() assert promiscuous_client_s333.sample_statistics() == SessionStatistics() client_requester.enable_feedback( feedback_collector.give) # FEEDBACK ENABLED HERE # Will fail with an error; make sure it's counted properly. The feedback registry entry will remain pending! media.raise_on_send_once(RuntimeError("Induced failure")) with pytest.raises(RuntimeError, match="Induced failure"): assert await client_requester.send( Transfer(timestamp=ts, priority=Priority.FAST, transfer_id=12, fragmented_payload=[]), tr.loop.time() + 1.0) assert client_requester.sample_statistics() == SessionStatistics( transfers=1, frames=1, payload_bytes=0, errors=1) # Some malformed feedback frames which will be ignored media.inject_received([ Envelope( UAVCANFrame( identifier=ServiceCANID( priority=Priority.FAST, source_node_id=5, destination_node_id=123, service_id=333, request_not_response=True, ).compile([_mem("Ignored")]), padded_payload=_mem("Ignored"), start_of_transfer=False, # Ignored because not start-of-frame end_of_transfer=False, toggle_bit=True, transfer_id=12, ).compile(), loopback=True, ) ]) media.inject_received([ Envelope( UAVCANFrame( identifier=ServiceCANID( priority=Priority.FAST, source_node_id=5, destination_node_id=123, service_id=333, request_not_response=True, ).compile([_mem("Ignored")]), padded_payload=_mem("Ignored"), start_of_transfer=True, end_of_transfer=False, toggle_bit=True, transfer_id=9, ).compile( ), # Ignored because there is no such transfer-ID in the registry loopback=True, ) ]) # Now, this transmission will succeed, but a pending loopback registry entry will be overwritten, which will be # reflected in the error counter. with caplog.at_level(logging.CRITICAL, logger=pyuavcan.transport.can.__name__): assert await client_requester.send( Transfer( timestamp=ts, priority=Priority.FAST, transfer_id=12, fragmented_payload=[ _mem( "Until philosophers are kings, or the kings and princes of this world have the spirit and " "power of philosophy, and political greatness and wisdom meet in one, and those commoner " "natures who pursue either to the exclusion of the other are compelled to stand aside, " "cities will never have rest from their evils "), _mem("- no, nor the human race, as I believe - "), _mem( "and then only will this our State have a possibility of life and behold the light of day." ), ], ), tr.loop.time() + 1.0, ) client_requester.disable_feedback() assert client_requester.sample_statistics() == SessionStatistics( transfers=2, frames=8, payload_bytes=438, errors=2) # The feedback is disabled, but we will send a valid loopback frame anyway to make sure it is silently ignored media.inject_received([ Envelope( UAVCANFrame( identifier=ServiceCANID( priority=Priority.FAST, source_node_id=5, destination_node_id=123, service_id=333, request_not_response=True, ).compile([_mem("Ignored")]), padded_payload=_mem("Ignored"), start_of_transfer=True, end_of_transfer=False, toggle_bit=True, transfer_id=12, ).compile(), loopback=True, ) ]) client_requester.close() with pytest.raises(ResourceClosedError): assert await client_requester.send( Transfer(timestamp=ts, priority=Priority.LOW, transfer_id=4, fragmented_payload=[]), tr.loop.time() + 1.0) fb = feedback_collector.take() assert fb.original_transfer_timestamp == ts validate_timestamp(fb.first_frame_transmission_timestamp) received = await promiscuous_server_s333.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.source_node_id == 5 assert received.transfer_id == 12 assert received.priority == Priority.FAST validate_timestamp(received.timestamp) assert len(received.fragmented_payload) == 7 # Equals the number of frames assert sum(map( len, received.fragmented_payload)) == 438 + 1 # Padding also included assert b"Until philosophers are kings" in bytes( received.fragmented_payload[0]) assert b"behold the light of day." in bytes( received.fragmented_payload[-1]) received = await selective_server_s333_5.receive(tr.loop.time() + 1.0 ) # Same thing here assert received is not None assert received.transfer_id == 12 assert received.priority == Priority.FAST validate_timestamp(received.timestamp) assert len(received.fragmented_payload) == 7 # Equals the number of frames assert sum(map( len, received.fragmented_payload)) == 438 + 1 # Padding also included assert b"Until philosophers are kings" in bytes( received.fragmented_payload[0]) assert b"behold the light of day." in bytes( received.fragmented_payload[-1]) # Nothing is received - non-matching node ID selector assert (await selective_server_s333_9.receive(tr.loop.time() + _RX_TIMEOUT)) is None # Nothing is received - non-matching role (not server) assert (await selective_client_s333_5.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert (await selective_client_s333_9.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert (await promiscuous_client_s333.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert selective_client_s333_5.sample_statistics() == SessionStatistics() assert selective_client_s333_9.sample_statistics() == SessionStatistics() assert promiscuous_client_s333.sample_statistics() == SessionStatistics() # Final transport stats check; additional loopback frames are due to our manual tests above assert tr.sample_statistics() == can.CANTransportStatistics( out_frames=16, out_frames_loopback=2, in_frames_loopback=5) assert tr2.sample_statistics() == can.CANTransportStatistics( in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15) # # Drop non-UAVCAN frames silently # media.inject_received([ can.media.DataFrame( identifier=ServiceCANID( priority=Priority.FAST, source_node_id=5, destination_node_id=123, service_id=333, request_not_response=True, ).compile([_mem("")]), data=bytearray( b"" ), # The CAN ID is valid for UAVCAN, but the payload is not - no tail byte format=can.media.FrameFormat.EXTENDED, ) ]) media.inject_received([ can.media.DataFrame( identifier=0, # The CAN ID is not valid for UAVCAN data=bytearray(b"123"), format=can.media.FrameFormat.BASE, ) ]) media.inject_received([ Envelope( UAVCANFrame( identifier=ServiceCANID( priority=Priority.FAST, source_node_id=5, destination_node_id=123, service_id=444, # No such service request_not_response=True, ).compile([_mem("Ignored")]), padded_payload=_mem("Ignored"), start_of_transfer=True, end_of_transfer=False, toggle_bit=True, transfer_id=12, ).compile(), loopback=True, ) ]) assert tr.sample_statistics() == can.CANTransportStatistics( out_frames=16, in_frames=2, out_frames_loopback=2, in_frames_loopback=6) assert tr2.sample_statistics() == can.CANTransportStatistics( in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15) # # Reception logic test. # pub_m2222 = tr2.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2222), None), meta) # Transfer ID timeout configuration - one of them will be configured very short for testing purposes subscriber_promiscuous.transfer_id_timeout = 1e-9 # Very low, basically zero timeout with pytest.raises(ValueError): subscriber_promiscuous.transfer_id_timeout = -1 with pytest.raises(ValueError): subscriber_promiscuous.transfer_id_timeout = float("nan") assert subscriber_promiscuous.transfer_id_timeout == pytest.approx(1e-9) subscriber_selective.transfer_id_timeout = 1.0 with pytest.raises(ValueError): subscriber_selective.transfer_id_timeout = -1 with pytest.raises(ValueError): subscriber_selective.transfer_id_timeout = float("nan") assert subscriber_selective.transfer_id_timeout == pytest.approx(1.0) # Queue capacity configuration assert subscriber_selective.frame_queue_capacity is None # Unlimited by default subscriber_selective.frame_queue_capacity = 2 with pytest.raises(ValueError): subscriber_selective.frame_queue_capacity = 0 assert subscriber_selective.frame_queue_capacity == 2 assert await pub_m2222.send( Transfer( timestamp=ts, priority=Priority.EXCEPTIONAL, transfer_id=7, fragmented_payload=[ _mem("Finally, from so little sleeping and so much reading, "), _mem( "his brain dried up and he went completely out of his mind." ), # Two frames. ], ), tr.loop.time() + 1.0, ) assert tr.sample_statistics() == can.CANTransportStatistics( out_frames=16, in_frames=4, in_frames_uavcan=2, in_frames_uavcan_accepted=2, out_frames_loopback=2, in_frames_loopback=6, ) assert tr2.sample_statistics() == can.CANTransportStatistics( out_frames=2, in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15) received = await subscriber_promiscuous.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.source_node_id == 123 assert received.priority == Priority.EXCEPTIONAL assert received.transfer_id == 7 validate_timestamp(received.timestamp) assert bytes(received.fragmented_payload[0]).startswith(b"Finally") assert bytes(received.fragmented_payload[-1]).rstrip(b"\x00").endswith( b"out of his mind.") received = await subscriber_selective.receive(tr.loop.time() + 1.0) assert received is not None assert received.priority == Priority.EXCEPTIONAL assert received.transfer_id == 7 validate_timestamp(received.timestamp) assert bytes(received.fragmented_payload[0]).startswith(b"Finally") assert bytes(received.fragmented_payload[-1]).rstrip(b"\x00").endswith( b"out of his mind.") assert subscriber_selective.sample_statistics( ) == subscriber_promiscuous.sample_statistics() assert subscriber_promiscuous.sample_statistics() == SessionStatistics( transfers=1, frames=2, payload_bytes=124) # Includes padding! # Small delay is needed to make the small-TID instance certainly time out on Windows, where clock resolution is low. await asyncio.sleep(0.1) assert await pub_m2222.send( Transfer( timestamp=ts, priority=Priority.NOMINAL, transfer_id= 7, # Same transfer ID, will be accepted only by the instance with low TID timeout fragmented_payload=[], ), tr.loop.time() + 1.0, ) assert tr.sample_statistics() == can.CANTransportStatistics( out_frames=16, in_frames=5, in_frames_uavcan=3, in_frames_uavcan_accepted=3, out_frames_loopback=2, in_frames_loopback=6, ) assert tr2.sample_statistics() == can.CANTransportStatistics( out_frames=3, in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15) received = await subscriber_promiscuous.receive(tr.loop.time() + 1.0) assert received is not None assert isinstance(received, TransferFrom) assert received.source_node_id == 123 assert received.priority == Priority.NOMINAL assert received.transfer_id == 7 validate_timestamp(received.timestamp) assert b"".join(received.fragmented_payload) == b"" assert subscriber_promiscuous.sample_statistics() == SessionStatistics( transfers=2, frames=3, payload_bytes=124) # Discarded because of the same transfer ID assert (await subscriber_selective.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert subscriber_selective.sample_statistics() == SessionStatistics( transfers=1, frames=3, payload_bytes=124, errors=1 # Error due to the repeated transfer ID ) assert await pub_m2222.send( Transfer( timestamp=ts, priority=Priority.HIGH, transfer_id=8, fragmented_payload=[ _mem("a" * 63), _mem("b" * 63), _mem("c" * 63), _mem( "d" * 62 ), # Tricky case - one of the CRC bytes spills over into the fifth frame ], ), tr.loop.time() + 1.0, ) # The promiscuous one is able to receive the transfer since its queue is large enough received = await subscriber_promiscuous.receive(tr.loop.time() + 1.0) assert received is not None assert received.priority == Priority.HIGH assert received.transfer_id == 8 validate_timestamp(received.timestamp) assert list(map(bytes, received.fragmented_payload)) == [ b"a" * 63, b"b" * 63, b"c" * 63, b"d" * 62, ] assert subscriber_promiscuous.sample_statistics() == SessionStatistics( transfers=3, frames=8, payload_bytes=375) # The selective one is unable to do so since its RX queue is too small; it is reflected in the error counter assert (await subscriber_selective.receive(tr.loop.time() + _RX_TIMEOUT)) is None assert subscriber_selective.sample_statistics() == SessionStatistics( transfers=1, frames=5, payload_bytes=124, errors=1, drops=3) # Overruns! # # Finalization. # print("str(CANTransport):", tr) print("str(CANTransport):", tr2) client_listener.close() server_listener.close() subscriber_promiscuous.close() subscriber_selective.close() tr.close() tr2.close() # Double-close has no effect: client_listener.close() server_listener.close() subscriber_promiscuous.close() subscriber_selective.close() tr.close() tr2.close() await asyncio.sleep( 1 ) # Let all pending tasks finalize properly to avoid stack traces in the output.