def serial_tunneled_via_tcp() -> typing.Iterator[TransportFactory]: from pyuavcan.transport.serial import SerialTransport from tests.transport.serial import VIRTUAL_BUS_URI yield lambda nid_a, nid_b: ( SerialTransport(VIRTUAL_BUS_URI, nid_a), SerialTransport(VIRTUAL_BUS_URI, nid_b), True, )
def make_udp_serial(nid: typing.Optional[int]) -> pyuavcan.transport.Transport: tr = RedundantTransport() if nid is not None: tr.attach_inferior(UDPTransport(f"127.0.0.{nid}")) else: tr.attach_inferior(UDPTransport(f"127.0.0.1", anonymous=True)) tr.attach_inferior(SerialTransport("socket://localhost:50905", local_node_id=nid)) return tr
def one(nid: typing.Optional[int]) -> RedundantTransport: red = RedundantTransport() red.attach_inferior( UDPTransport(f'127.0.0.{nid}/8') if nid is not None else UDPTransport('127.255.255.255/8')) red.attach_inferior(SerialTransport(VIRTUAL_BUS_URI, nid)) print('REDUNDANT TRANSPORT UDP+SERIAL:', red) return red
def one(nid: typing.Optional[int]) -> RedundantTransport: red = RedundantTransport() if nid is not None: red.attach_inferior(UDPTransport(f"127.0.0.{nid}")) else: red.attach_inferior(UDPTransport("127.0.0.1", anonymous=True)) red.attach_inferior(SerialTransport(VIRTUAL_BUS_URI, nid)) print("UDP+SERIAL:", red) return red
def _make_serial( registers: MutableMapping[str, ValueProxy], node_id: Optional[int]) -> Iterator[pyuavcan.transport.Transport]: def init(name: str, default: RelaxedValue) -> ValueProxy: return registers.setdefault("uavcan.serial." + name, ValueProxy(default)) port_list = str(init("iface", "")).split() srv_mult = int(init("duplicate_service_transfers", False)) + 1 baudrate = int(init("baudrate", Natural32([0]))) or None if port_list: from pyuavcan.transport.serial import SerialTransport for port in port_list: yield SerialTransport(str(port), node_id, service_transfer_multiplier=srv_mult, baudrate=baudrate)
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.
def _get_run_configs() -> typing.Iterable[RunConfig]: """ Provides interface options to test the demo against. When adding new transports, add them to the demo and update this factory accordingly. Don't forget about redundant configurations, too. """ from pyuavcan.transport.redundant import RedundantTransport from pyuavcan.transport.serial import SerialTransport from pyuavcan.transport.udp import UDPTransport # UDP yield RunConfig( demo_env_vars={"DEMO_INTERFACE_KIND": "udp"}, local_transport_factory=lambda nid: UDPTransport(f"127.0.0.{1 if nid is None else nid}", anonymous=nid is None), ) # Serial yield RunConfig( demo_env_vars={"DEMO_INTERFACE_KIND": "serial"}, local_transport_factory=lambda nid: SerialTransport("socket://localhost:50905", local_node_id=nid), ) # DMR UDP+Serial def make_udp_serial(nid: typing.Optional[int]) -> pyuavcan.transport.Transport: tr = RedundantTransport() if nid is not None: tr.attach_inferior(UDPTransport(f"127.0.0.{nid}")) else: tr.attach_inferior(UDPTransport(f"127.0.0.1", anonymous=True)) tr.attach_inferior(SerialTransport("socket://localhost:50905", local_node_id=nid)) return tr yield RunConfig( demo_env_vars={"DEMO_INTERFACE_KIND": "udp_serial"}, local_transport_factory=make_udp_serial, ) if sys.platform.startswith("linux"): from pyuavcan.transport.can.media.socketcan import SocketCANMedia from pyuavcan.transport.can import CANTransport # CAN yield RunConfig( demo_env_vars={"DEMO_INTERFACE_KIND": "can"}, # The demo uses Classic CAN! SocketCAN does not support nonuniform MTU well. local_transport_factory=lambda nid: CANTransport(SocketCANMedia("vcan0", 8), local_node_id=nid), ) # TMR CAN def make_tmr_can(nid: typing.Optional[int]) -> pyuavcan.transport.Transport: from pyuavcan.transport.redundant import RedundantTransport tr = RedundantTransport() tr.attach_inferior(CANTransport(SocketCANMedia("vcan0", 8), local_node_id=nid)) tr.attach_inferior(CANTransport(SocketCANMedia("vcan1", 32), local_node_id=nid)) tr.attach_inferior(CANTransport(SocketCANMedia("vcan2", 64), local_node_id=nid)) return tr yield RunConfig( demo_env_vars={"DEMO_INTERFACE_KIND": "can_can_can"}, local_transport_factory=make_tmr_can, )
async def _unittest_redundant_transport_capture() -> None: from threading import Lock from pyuavcan.transport import Capture, Trace, TransferTrace, Priority, ServiceDataSpecifier from pyuavcan.transport import AlienTransfer, AlienTransferMetadata, AlienSessionSpecifier from pyuavcan.transport.redundant import RedundantDuplicateTransferTrace, RedundantCapture from tests.transport.can.media.mock import MockMedia as CANMockMedia asyncio.get_event_loop().slow_callback_duration = 5.0 tracer = RedundantTransport.make_tracer() traces: typing.List[typing.Optional[Trace]] = [] lock = Lock() def handle_capture(cap: Capture) -> None: with lock: # Drop TX frames, they are not interesting for this test. assert isinstance(cap, RedundantCapture) if isinstance(cap.inferior, pyuavcan.transport.serial.SerialCapture ) and cap.inferior.own: return if isinstance( cap.inferior, pyuavcan.transport.can.CANCapture) and cap.inferior.own: return print("CAPTURE:", cap) traces.append(tracer.update(cap)) async def wait(how_many: int) -> None: for _ in range(10): await asyncio.sleep(0.1) with lock: if len(traces) >= how_many: return assert False, "No traces received" # Setup capture -- one is added before capture started, the other is added later. # Make sure they are treated identically. tr = RedundantTransport() inf_a: pyuavcan.transport.Transport = SerialTransport(SERIAL_URI, 1234) inf_b: pyuavcan.transport.Transport = SerialTransport(SERIAL_URI, 1234) tr.attach_inferior(inf_a) assert not tr.capture_active assert not inf_a.capture_active assert not inf_b.capture_active tr.begin_capture(handle_capture) assert tr.capture_active assert inf_a.capture_active assert not inf_b.capture_active tr.attach_inferior(inf_b) assert tr.capture_active assert inf_a.capture_active assert inf_b.capture_active # Send a transfer and make sure it is handled and deduplicated correctly. transfer = AlienTransfer( AlienTransferMetadata( priority=Priority.IMMEDIATE, transfer_id=1234, session_specifier=AlienSessionSpecifier( source_node_id=321, destination_node_id=222, data_specifier=ServiceDataSpecifier( 77, ServiceDataSpecifier.Role.REQUEST), ), ), [memoryview(b"hello")], ) assert await tr.spoof(transfer, monotonic_deadline=asyncio.get_event_loop().time() + 1.0) await wait(2) with lock: # Check the status of the deduplication process. We should get two: one transfer, one duplicate. assert len(traces) == 2 trace = traces.pop(0) assert isinstance(trace, TransferTrace) assert trace.transfer == transfer # This is the duplicate. assert isinstance(traces.pop(0), RedundantDuplicateTransferTrace) assert not traces # Spoof the same thing again, get nothing out: transfers discarded by the inferior's own reassemblers. # WARNING: this will fail if too much time has passed since the previous transfer due to TID timeout. assert await tr.spoof(transfer, monotonic_deadline=asyncio.get_event_loop().time() + 1.0) await wait(2) with lock: assert None is traces.pop(0) assert None is traces.pop(0) assert not traces # But if we change ONLY destination, deduplication will not take place. transfer = AlienTransfer( AlienTransferMetadata( priority=Priority.IMMEDIATE, transfer_id=1234, session_specifier=AlienSessionSpecifier( source_node_id=321, destination_node_id=333, data_specifier=ServiceDataSpecifier( 77, ServiceDataSpecifier.Role.REQUEST), ), ), [memoryview(b"hello")], ) assert await tr.spoof(transfer, monotonic_deadline=asyncio.get_event_loop().time() + 1.0) await wait(2) with lock: # Check the status of the deduplication process. We should get two: one transfer, one duplicate. assert len(traces) == 2 trace = traces.pop(0) assert isinstance(trace, TransferTrace) assert trace.transfer == transfer # This is the duplicate. assert isinstance(traces.pop(0), RedundantDuplicateTransferTrace) assert not traces # Change the inferior configuration and make sure it is handled properly. tr.detach_inferior(inf_a) tr.detach_inferior(inf_b) inf_a.close() inf_b.close() # The new inferiors use cyclic transfer-ID; the tracer should reconfigure itself automatically! can_peers: typing.Set[CANMockMedia] = set() inf_a = CANTransport(CANMockMedia(can_peers, 64, 2), 111) inf_b = CANTransport(CANMockMedia(can_peers, 64, 2), 111) tr.attach_inferior(inf_a) tr.attach_inferior(inf_b) # Capture should have been launched automatically. assert inf_a.capture_active assert inf_b.capture_active # Send transfer over CAN and observe that it is handled well. transfer = AlienTransfer( AlienTransferMetadata( priority=Priority.IMMEDIATE, transfer_id=19, session_specifier=AlienSessionSpecifier( source_node_id=111, destination_node_id=22, data_specifier=ServiceDataSpecifier( 77, ServiceDataSpecifier.Role.REQUEST), ), ), [memoryview(b"hello")], ) assert await tr.spoof(transfer, monotonic_deadline=asyncio.get_event_loop().time() + 1.0) await wait(2) with lock: # Check the status of the deduplication process. We should get two: one transfer, one duplicate. assert len(traces) == 2 trace = traces.pop(0) assert isinstance(trace, TransferTrace) assert trace.transfer == transfer # This is the duplicate. assert isinstance(traces.pop(0), RedundantDuplicateTransferTrace) assert not traces # Dispose of everything. tr.close() await asyncio.sleep(1.0)
def _unittest_serial_tracer() -> None: from pytest import raises, approx from pyuavcan.transport import Priority, MessageDataSpecifier from pyuavcan.transport.serial import SerialTransport tr = SerialTransport.make_tracer() ts = Timestamp.now() def tx( x: typing.Union[bytes, bytearray, memoryview]) -> typing.Optional[Trace]: return tr.update( SerialCapture(ts, SerialCapture.Direction.TX, memoryview(x))) def rx( x: typing.Union[bytes, bytearray, memoryview]) -> typing.Optional[Trace]: return tr.update( SerialCapture(ts, SerialCapture.Direction.RX, memoryview(x))) buf = SerialFrame( priority=Priority.SLOW, transfer_id=1234567890, index=0, end_of_transfer=True, payload=memoryview(b"abc"), source_node_id=1111, destination_node_id=None, data_specifier=MessageDataSpecifier(6666), ).compile_into(bytearray(100)) head, tail = buf[:10], buf[10:] assert None is tx(head) # Semi-complete. trace = tx(head) # Double-head invalidates the previous one. assert isinstance(trace, SerialOutOfBandTrace) assert trace.timestamp == ts assert trace.data.tobytes().strip(b"\0") == head.tobytes().strip(b"\0") trace = tx(tail) 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 == 1111 assert trace.transfer.metadata.session_specifier.destination_node_id is None assert trace.transfer.metadata.session_specifier.data_specifier == MessageDataSpecifier( 6666) assert trace.transfer.fragmented_payload == [memoryview(b"abc")] buf = SerialFrame( priority=Priority.SLOW, transfer_id=1234567890, index=0, end_of_transfer=True, payload=memoryview(b"abc"), source_node_id=None, destination_node_id=None, data_specifier=MessageDataSpecifier(6666), ).compile_into(bytearray(100)) trace = rx(buf) assert isinstance(trace, TransferTrace) assert trace.timestamp == ts assert trace.transfer.metadata.transfer_id == 1234567890 assert trace.transfer.metadata.session_specifier.source_node_id is None assert trace.transfer.metadata.session_specifier.destination_node_id is None assert None is tr.update( pyuavcan.transport.Capture(ts)) # Wrong type, ignore. trace = tx( SerialFrame( priority=Priority.SLOW, transfer_id=1234567890, index=0, end_of_transfer=False, payload=memoryview(bytes(range(256))), source_node_id=3333, destination_node_id=None, data_specifier=MessageDataSpecifier(6666), ).compile_into(bytearray(10_000))) assert trace is None trace = tx( SerialFrame( priority=Priority.SLOW, transfer_id=1234567890, index=1, end_of_transfer=True, payload=memoryview(bytes(range(256))), source_node_id=3333, destination_node_id=None, data_specifier=MessageDataSpecifier(6666), ).compile_into(bytearray(10_000))) assert isinstance(trace, SerialErrorTrace) assert trace.error == TransferReassembler.Error.MULTIFRAME_INTEGRITY_ERROR with raises(ValueError, match=".*delimiters.*"): rx(b"".join([buf, buf]))
async def _unittest_serial_transport_capture(caplog: typing.Any) -> None: from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer from pyuavcan.transport import Priority, Timestamp, OutputSessionSpecifier get_monotonic = asyncio.get_event_loop().time tr = SerialTransport(serial_port="loop://", local_node_id=42, mtu=1024, service_transfer_multiplier=2) sft_capacity = 1024 payload_single = [_mem("qwertyui"), _mem("01234567") ] * (sft_capacity // 16) assert sum(map(len, payload_single)) == sft_capacity payload_x3 = (payload_single * 3)[:-1] payload_x3_size_bytes = sft_capacity * 3 - 8 assert sum(map(len, payload_x3)) == payload_x3_size_bytes broadcaster = tr.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), PayloadMetadata(10000)) client_requester = tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 3210), PayloadMetadata(10000), ) events: typing.List[SerialCapture] = [] events2: typing.List[pyuavcan.transport.Capture] = [] def append_events(cap: pyuavcan.transport.Capture) -> None: assert isinstance(cap, SerialCapture) events.append(cap) tr.begin_capture(append_events) tr.begin_capture(events2.append) assert events == [] assert events2 == [] # # Multi-frame message. # ts = Timestamp.now() assert await broadcaster.send( Transfer(timestamp=ts, priority=Priority.LOW, transfer_id=777, fragmented_payload=payload_x3), monotonic_deadline=get_monotonic() + 5.0, ) await asyncio.sleep(0.1) assert events == events2 # Send three, receive three. # Sorting is required because the ordering of the events in the middle is not defined: arrival events # may or may not be registered before the emission event depending on how the serial loopback is operating. a, b, c, d, e, f = sorted(events, key=lambda x: not x.own) assert isinstance(a, SerialCapture) and a.own assert isinstance(b, SerialCapture) and b.own assert isinstance(c, SerialCapture) and c.own assert isinstance(d, SerialCapture) and not d.own assert isinstance(e, SerialCapture) and not e.own assert isinstance(f, SerialCapture) and not f.own def parse(x: SerialCapture) -> SerialFrame: out = SerialFrame.parse_from_cobs_image(x.fragment) assert out is not None return out assert parse(a).transfer_id == 777 assert parse(b).transfer_id == 777 assert parse(c).transfer_id == 777 assert a.timestamp.monotonic >= ts.monotonic assert b.timestamp.monotonic >= ts.monotonic assert c.timestamp.monotonic >= ts.monotonic assert parse(a).index == 0 assert parse(b).index == 1 assert parse(c).index == 2 assert not parse(a).end_of_transfer assert not parse(b).end_of_transfer assert parse(c).end_of_transfer assert a.fragment.tobytes().strip(b"\x00") == d.fragment.tobytes().strip( b"\x00") assert b.fragment.tobytes().strip(b"\x00") == e.fragment.tobytes().strip( b"\x00") assert c.fragment.tobytes().strip(b"\x00") == f.fragment.tobytes().strip( b"\x00") events.clear() events2.clear() # # Single-frame service request with dual frame duplication. # ts = Timestamp.now() assert await client_requester.send( Transfer(timestamp=ts, priority=Priority.HIGH, transfer_id=888, fragmented_payload=payload_single), monotonic_deadline=get_monotonic() + 5.0, ) await asyncio.sleep(0.1) assert events == events2 # Send two, receive two. # Sorting is required because the order of the two events in the middle is not defined: the arrival event # may or may not be registered before the emission event depending on how the serial loopback is operating. a, b, c, d = sorted(events, key=lambda x: not x.own) assert isinstance(a, SerialCapture) and a.own assert isinstance(b, SerialCapture) and b.own assert isinstance(c, SerialCapture) and not c.own assert isinstance(d, SerialCapture) and not d.own assert parse(a).transfer_id == 888 assert parse(b).transfer_id == 888 assert a.timestamp.monotonic >= ts.monotonic assert b.timestamp.monotonic >= ts.monotonic assert parse(a).index == 0 assert parse(b).index == 0 assert parse(a).end_of_transfer assert parse(b).end_of_transfer assert a.fragment.tobytes().strip(b"\x00") == c.fragment.tobytes().strip( b"\x00") assert b.fragment.tobytes().strip(b"\x00") == d.fragment.tobytes().strip( b"\x00") events.clear() events2.clear() # # Out-of-band data. # grownups = b"Aren't there any grownups at all? - No grownups!\x00" with caplog.at_level(logging.CRITICAL, logger=pyuavcan.transport.serial.__name__): # The frame delimiter is needed to force new frame into the state machine. tr.serial_port.write(grownups) await asyncio.sleep(1) assert events == events2 (oob, ) = events assert isinstance(oob, SerialCapture) assert not oob.own assert bytes(oob.fragment) == grownups events.clear() events2.clear()
async def _unittest_serial_transport(caplog: typing.Any) -> None: from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer, TransferFrom from pyuavcan.transport import Priority, Timestamp, InputSessionSpecifier, OutputSessionSpecifier from pyuavcan.transport import ProtocolParameters get_monotonic = asyncio.get_event_loop().time service_multiplication_factor = 2 with pytest.raises(ValueError): _ = SerialTransport(serial_port="loop://", local_node_id=None, mtu=1) with pytest.raises(ValueError): _ = SerialTransport(serial_port="loop://", local_node_id=None, service_transfer_multiplier=10000) with pytest.raises(pyuavcan.transport.InvalidMediaConfigurationError): _ = SerialTransport(serial_port=serial.serial_for_url( "loop://", do_not_open=True), local_node_id=None) tr = SerialTransport(serial_port="loop://", local_node_id=None, mtu=1024) assert tr.local_node_id is None assert tr.serial_port.is_open assert tr.input_sessions == [] assert tr.output_sessions == [] assert tr.protocol_parameters == ProtocolParameters( transfer_id_modulo=2**64, max_nodes=4096, mtu=1024, ) assert tr.sample_statistics() == SerialTransportStatistics() sft_capacity = 1024 payload_single = [_mem("qwertyui"), _mem("01234567") ] * (sft_capacity // 16) assert sum(map(len, payload_single)) == sft_capacity payload_x3 = (payload_single * 3)[:-1] payload_x3_size_bytes = sft_capacity * 3 - 8 assert sum(map(len, payload_x3)) == payload_x3_size_bytes # # Instantiate session objects. # meta = PayloadMetadata(10000) 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(2345), None), meta) assert subscriber_promiscuous is tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), None), meta) subscriber_selective = tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta) assert subscriber_selective is tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), 3210), 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) client_listener = tr.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 3210), meta) assert client_listener is tr.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 3210), meta) print("INPUTS:", tr.input_sessions) print("OUTPUTS:", tr.output_sessions) assert set(tr.input_sessions) == { subscriber_promiscuous, subscriber_selective, server_listener, client_listener } assert set(tr.output_sessions) == {broadcaster} assert tr.sample_statistics() == SerialTransportStatistics() # # Message exchange test. # assert await broadcaster.send( Transfer(timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=77777, fragmented_payload=payload_single), monotonic_deadline=get_monotonic() + 5.0, ) rx_transfer = await subscriber_promiscuous.receive(get_monotonic() + 5.0) print("PROMISCUOUS SUBSCRIBER TRANSFER:", rx_transfer) assert isinstance(rx_transfer, TransferFrom) assert rx_transfer.priority == Priority.LOW assert rx_transfer.transfer_id == 77777 assert rx_transfer.fragmented_payload == [b"".join(payload_single)] print(tr.sample_statistics()) assert tr.sample_statistics().in_bytes >= 32 + sft_capacity + 2 assert tr.sample_statistics().in_frames == 1 assert tr.sample_statistics().in_out_of_band_bytes == 0 assert tr.sample_statistics().out_bytes == tr.sample_statistics().in_bytes assert tr.sample_statistics().out_frames == 1 assert tr.sample_statistics().out_transfers == 1 assert tr.sample_statistics().out_incomplete == 0 with pytest.raises( pyuavcan.transport.OperationNotDefinedForAnonymousNodeError): # Anonymous nodes can't send multiframe transfers. assert await broadcaster.send( Transfer(timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=77777, fragmented_payload=payload_x3), monotonic_deadline=get_monotonic() + 5.0, ) assert None is await subscriber_selective.receive(get_monotonic() + 0.1) assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1) assert None is await server_listener.receive(get_monotonic() + 0.1) assert None is await client_listener.receive(get_monotonic() + 0.1) # # Service exchange test. # with pytest.raises( pyuavcan.transport.OperationNotDefinedForAnonymousNodeError): # Anonymous nodes can't emit service transfers. tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 3210), meta) # # Replace the transport with a different one where the local node-ID is not None. # tr = SerialTransport(serial_port="loop://", local_node_id=3210, mtu=1024) assert tr.local_node_id == 3210 # # Re-instantiate session objects because the transport instances have been replaced. # 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(2345), None), meta) subscriber_selective = tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), 3210), 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), 3210), meta) assert server_responder is tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 3210), meta) client_requester = tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 3210), meta) assert client_requester is tr.get_output_session( OutputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 3210), meta) client_listener = tr.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 3210), meta) assert client_listener is tr.get_input_session( InputSessionSpecifier( ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 3210), meta) assert set(tr.input_sessions) == { subscriber_promiscuous, subscriber_selective, server_listener, client_listener } assert set(tr.output_sessions) == { broadcaster, server_responder, client_requester } assert tr.sample_statistics() == SerialTransportStatistics() assert await client_requester.send( Transfer(timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=88888, fragmented_payload=payload_x3), monotonic_deadline=get_monotonic() + 5.0, ) rx_transfer = await server_listener.receive(get_monotonic() + 5.0) print("SERVER LISTENER TRANSFER:", rx_transfer) assert isinstance(rx_transfer, TransferFrom) assert rx_transfer.priority == Priority.HIGH assert rx_transfer.transfer_id == 88888 assert len(rx_transfer.fragmented_payload) == 3 assert b"".join(rx_transfer.fragmented_payload) == b"".join(payload_x3) assert None is await subscriber_selective.receive(get_monotonic() + 0.1) assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1) assert None is await server_listener.receive(get_monotonic() + 0.1) assert None is await client_listener.receive(get_monotonic() + 0.1) print(tr.sample_statistics()) assert tr.sample_statistics().in_bytes >= ( 32 * 3 + payload_x3_size_bytes + 2) * service_multiplication_factor assert tr.sample_statistics( ).in_frames == 3 * service_multiplication_factor assert tr.sample_statistics().in_out_of_band_bytes == 0 assert tr.sample_statistics().out_bytes == tr.sample_statistics().in_bytes assert tr.sample_statistics( ).out_frames == 3 * service_multiplication_factor assert tr.sample_statistics( ).out_transfers == 1 * service_multiplication_factor assert tr.sample_statistics().out_incomplete == 0 # # Write timeout test. # assert not await broadcaster.send( Transfer(timestamp=Timestamp.now(), priority=Priority.IMMEDIATE, transfer_id=99999, fragmented_payload=payload_x3), monotonic_deadline=get_monotonic() - 5.0, # The deadline is in the past. ) assert None is await subscriber_selective.receive(get_monotonic() + 0.1) assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1) assert None is await server_listener.receive(get_monotonic() + 0.1) assert None is await client_listener.receive(get_monotonic() + 0.1) print(tr.sample_statistics()) assert tr.sample_statistics().in_bytes >= ( 32 * 3 + payload_x3_size_bytes + 2) * service_multiplication_factor assert tr.sample_statistics( ).in_frames == 3 * service_multiplication_factor assert tr.sample_statistics().in_out_of_band_bytes == 0 assert tr.sample_statistics().out_bytes == tr.sample_statistics().in_bytes assert tr.sample_statistics( ).out_frames == 3 * service_multiplication_factor assert tr.sample_statistics( ).out_transfers == 1 * service_multiplication_factor assert tr.sample_statistics().out_incomplete == 1 # INCREMENTED HERE # # Selective message exchange test. # assert await broadcaster.send( Transfer(timestamp=Timestamp.now(), priority=Priority.IMMEDIATE, transfer_id=99999, fragmented_payload=payload_x3), monotonic_deadline=get_monotonic() + 5.0, ) rx_transfer = await subscriber_promiscuous.receive(get_monotonic() + 5.0) print("PROMISCUOUS SUBSCRIBER TRANSFER:", rx_transfer) assert isinstance(rx_transfer, TransferFrom) assert rx_transfer.priority == Priority.IMMEDIATE assert rx_transfer.transfer_id == 99999 assert b"".join(rx_transfer.fragmented_payload) == b"".join(payload_x3) rx_transfer = await subscriber_selective.receive(get_monotonic() + 1.0) print("SELECTIVE SUBSCRIBER TRANSFER:", rx_transfer) assert isinstance(rx_transfer, TransferFrom) assert rx_transfer.priority == Priority.IMMEDIATE assert rx_transfer.transfer_id == 99999 assert b"".join(rx_transfer.fragmented_payload) == b"".join(payload_x3) assert None is await subscriber_selective.receive(get_monotonic() + 0.1) assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1) assert None is await server_listener.receive(get_monotonic() + 0.1) assert None is await client_listener.receive(get_monotonic() + 0.1) # # Out-of-band data test. # with caplog.at_level(logging.CRITICAL, logger=pyuavcan.transport.serial.__name__): stats_reference = tr.sample_statistics() # The frame delimiter is needed to force new frame into the state machine. grownups = b"Aren't there any grownups at all? - No grownups!\x00" tr.serial_port.write(grownups) stats_reference.in_bytes += len(grownups) stats_reference.in_out_of_band_bytes += len(grownups) # Wait for the reader thread to catch up. assert None is await subscriber_selective.receive(get_monotonic() + 0.2) assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.2) assert None is await server_listener.receive(get_monotonic() + 0.2) assert None is await client_listener.receive(get_monotonic() + 0.2) print(tr.sample_statistics()) assert tr.sample_statistics() == stats_reference # The frame delimiter is needed to force new frame into the state machine. tr.serial_port.write( bytes([0xFF, 0xFF, SerialFrame.FRAME_DELIMITER_BYTE])) stats_reference.in_bytes += 3 stats_reference.in_out_of_band_bytes += 3 # Wait for the reader thread to catch up. assert None is await subscriber_selective.receive(get_monotonic() + 0.2) assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.2) assert None is await server_listener.receive(get_monotonic() + 0.2) assert None is await client_listener.receive(get_monotonic() + 0.2) print(tr.sample_statistics()) assert tr.sample_statistics() == stats_reference # # Termination. # assert set(tr.input_sessions) == { subscriber_promiscuous, subscriber_selective, server_listener, client_listener } assert set(tr.output_sessions) == { broadcaster, server_responder, client_requester } subscriber_promiscuous.close() subscriber_promiscuous.close() # Idempotency. assert set(tr.input_sessions) == { subscriber_selective, server_listener, client_listener } assert set(tr.output_sessions) == { broadcaster, server_responder, client_requester } broadcaster.close() broadcaster.close() # Idempotency. assert set(tr.input_sessions) == { subscriber_selective, server_listener, client_listener } assert set(tr.output_sessions) == {server_responder, client_requester} tr.close() tr.close() # Idempotency. assert not set(tr.input_sessions) assert not set(tr.output_sessions) with pytest.raises(pyuavcan.transport.ResourceClosedError): _ = tr.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) with pytest.raises(pyuavcan.transport.ResourceClosedError): _ = tr.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), None), meta) await asyncio.sleep( 1 ) # Let all pending tasks finalize properly to avoid stack traces in the output.
def one(nid: typing.Optional[int]) -> RedundantTransport: red = RedundantTransport() red.attach_inferior(UDPTransport("127.0.0.1", local_node_id=nid)) red.attach_inferior(SerialTransport(VIRTUAL_BUS_URI, nid)) print("UDP+SERIAL:", red) return red