def one(nid: typing.Optional[int]) -> RedundantTransport: red = RedundantTransport() red.attach_inferior( CANTransport(SocketCANMedia("vcan0", 64), nid)) red.attach_inferior( CANTransport(SocketCANMedia("vcan1", 32), nid)) return red
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 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
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 one(nid: typing.Optional[int]) -> RedundantTransport: red = RedundantTransport() red.attach_inferior(CANTransport(MockMedia( bus_0, 8, 1), nid)) # Heterogeneous setup (CAN classic) red.attach_inferior(CANTransport(MockMedia( bus_1, 32, 2), nid)) # Heterogeneous setup (CAN FD) red.attach_inferior(CANTransport(MockMedia( bus_2, 64, 3), nid)) # Heterogeneous setup (CAN FD) return red
def _unittest_output_tid_file_path() -> None: from pyuavcan.transport.redundant import RedundantTransport from pyuavcan.transport.loopback import LoopbackTransport def once(tr: Transport) -> typing.Optional[pathlib.Path]: return _get_output_transfer_id_map_path(tr) assert once(LoopbackTransport(None)) is None assert once(LoopbackTransport(123)) == OUTPUT_TRANSFER_ID_MAP_DIR / "123" red = RedundantTransport() assert once(red) is None red.attach_inferior(LoopbackTransport(4000)) red.attach_inferior(LoopbackTransport(4000)) assert once(red) == OUTPUT_TRANSFER_ID_MAP_DIR / "4000" red = RedundantTransport() red.attach_inferior(LoopbackTransport(None)) red.attach_inferior(LoopbackTransport(None)) assert once(red) is None
def one(nid: typing.Optional[int]) -> RedundantTransport: # Triply redundant CAN bus. red = RedundantTransport() red.attach_inferior(CANTransport(MockMedia( bus_0, 8, 1), nid)) # Heterogeneous setup (CAN classic) red.attach_inferior(CANTransport(MockMedia(bus_1, 32, 1), nid)) # Heterogeneous setup (CAN FD) red.attach_inferior(CANTransport(MockMedia(bus_2, 64, 1), nid)) # Heterogeneous setup (CAN FD) print('REDUNDANT TRANSPORT CANx3:', red) return red
def init_transport() -> pyuavcan.transport.Transport: assert isinstance(registry, register.Registry) if transport is None: out = make_transport(registry, reconfigurable=reconfigurable_transport) if out is not None: return out raise MissingTransportConfigurationError( "Available registers do not encode a valid transport configuration" ) if not isinstance(transport, RedundantTransport) and reconfigurable_transport: out = RedundantTransport() out.attach_inferior(transport) return out return transport
def _unittest_redundant_transport_reconfiguration() -> None: from pyuavcan.transport import OutputSessionSpecifier, MessageDataSpecifier, PayloadMetadata tr = RedundantTransport() tr.attach_inferior(LoopbackTransport(1234)) ses = tr.get_output_session(OutputSessionSpecifier(MessageDataSpecifier(5555), None), PayloadMetadata(0)) assert ses tr.detach_inferior(tr.inferiors[0]) tr.attach_inferior(LoopbackTransport(1235)) # Different node-ID tr.detach_inferior(tr.inferiors[0]) tr.attach_inferior(LoopbackTransport(None, allow_anonymous_transfers=True)) # Anonymous with pytest.raises(pyuavcan.transport.OperationNotDefinedForAnonymousNodeError): tr.attach_inferior(LoopbackTransport(None, allow_anonymous_transfers=False)) assert len(tr.inferiors) == 1
def construct_transport(expression: str) -> Transport: context = _make_evaluation_context() trs = _evaluate_transport_expr(expression, context) _logger.debug("Transport expression evaluation result: %r", trs) if len(trs) == 1: return trs[0] # Non-redundant transport if len(trs) > 1: from pyuavcan.transport.redundant import RedundantTransport rt = RedundantTransport() for t in trs: rt.attach_inferior(t) assert rt.inferiors == trs return rt raise ValueError("No transports specified")
def _unittest_redundant_transport_capture() -> None: def mon(_x: object) -> None: return None tr = RedundantTransport() inf_a = LoopbackTransport(1234) inf_b = LoopbackTransport(1234) tr.begin_capture(mon) assert inf_a.capture_handlers == [] assert inf_b.capture_handlers == [] tr.attach_inferior(inf_a) assert inf_a.capture_handlers == [mon] assert inf_b.capture_handlers == [] tr.attach_inferior(inf_b) assert inf_a.capture_handlers == [mon] assert inf_b.capture_handlers == [mon]
def construct_subsystem(self, args: argparse.Namespace) -> pyuavcan.transport.Transport: context = _make_evaluation_context() _logger.debug('Expression evaluation context: %r', list(context.keys())) trs: typing.List[pyuavcan.transport.Transport] = [] if args.transport is not None: for expression in args.transport: t = _evaluate_transport_expr(expression, context) _logger.info('Expression %r yields %r', expression, t) trs.append(t) if len(trs) < 1: raise ValueError('No transports specified') elif len(trs) == 1: return trs[0] # Non-redundant transport else: from pyuavcan.transport.redundant import RedundantTransport rt = RedundantTransport() for t in trs: rt.attach_inferior(t) assert rt.inferiors == trs return rt
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 make_transport( registers: MutableMapping[str, ValueProxy], *, reconfigurable: bool = False, ) -> Optional[pyuavcan.transport.Transport]: """ Constructs a transport instance based on the configuration encoded in the supplied registers. If more than one transport is defined, a redundant instance will be constructed. The register schema is documented below per transport class (refer to the transport class documentation to find the defaults for optional registers). All transports also accept the following standard regsiters: +-------------------+-------------------+-----------------------------------------------------------------------+ | Register name | Register type | Semantics | +===================+===================+=======================================================================+ | ``uavcan.node.id``| ``natural16[1]`` | The node-ID to use. If the value exceeds the valid | | | | range, the constructed node will be anonymous. | +-------------------+-------------------+-----------------------------------------------------------------------+ .. list-table:: :mod:`pyuavcan.transport.udp` :widths: 1 1 9 :header-rows: 1 * - Register name - Register type - Register semantics * - ``uavcan.udp.iface`` - ``string`` - Whitespace-separated list of /16 IP subnet addresses. 16 least significant bits are replaced with the node-ID if configured, otherwise left unchanged. E.g.: ``127.42.0.42``: node-ID 257, result ``127.42.1.1``; ``127.42.0.42``: anonymous, result ``127.42.0.42``. * - ``uavcan.udp.duplicate_service_transfers`` - ``bit[1]`` - Apply deterministic data loss mitigation to RPC-service transfers by setting multiplication factor = 2. * - ``uavcan.udp.mtu`` - ``natural16[1]`` - The MTU for all constructed transport instances. .. list-table:: :mod:`pyuavcan.transport.serial` :widths: 1 1 9 :header-rows: 1 * - Register name - Register type - Register semantics * - ``uavcan.serial.iface`` - ``string`` - Whitespace-separated list of serial port names. E.g.: ``/dev/ttyACM0``, ``COM9``, ``socket://127.0.0.1:50905``. * - ``uavcan.serial.duplicate_service_transfers`` - ``bit[1]`` - Apply deterministic data loss mitigation to RPC-service transfers by setting multiplication factor = 2. * - ``uavcan.serial.baudrate`` - ``natural32[1]`` - The baudrate to set for all specified serial ports. Leave unchanged if zero. .. list-table:: :mod:`pyuavcan.transport.can` :widths: 1 1 9 :header-rows: 1 * - Register name - Register type - Register semantics * - ``uavcan.can.iface`` - ``string`` - Whitespace-separated list of CAN iface names. Each iface name shall follow the format defined in :class:`pyuavcan.transport.can.media.pythoncan`. E.g.: ``socketcan:vcan0``. * - ``uavcan.can.mtu`` - ``natural16[1]`` - The MTU value to use with all constructed CAN transports. Values other than 8 and 64 should not be used. * - ``uavcan.can.bitrate`` - ``natural32[2]`` - The bitrates to use for all constructed CAN transports for arbitration (first value) and data (second value) segments. To use Classic CAN, set both to the same value and set MTU = 8. .. list-table:: :mod:`pyuavcan.transport.loopback` :widths: 1 1 9 :header-rows: 1 * - Register name - Register type - Register semantics * - ``uavcan.loopback`` - ``bit[1]`` - If True, a loopback transport will be constructed. This is intended for testing only. :param registers: A mutable mapping of :class:`str` to :class:`pyuavcan.application.register.ValueProxy`. Normally, it should be constructed by :func:`pyuavcan.application.make_registry`. :param reconfigurable: If False (default), the return value is: - None if the registers do not encode a valid transport configuration. - A single transport instance if a non-redundant configuration is defined. - An instance of :class:`pyuavcan.transport.RedundantTransport` if more than one transport configuration is defined. If True, then the returned instance is always of type :class:`pyuavcan.transport.RedundantTransport`, where the set of inferiors is empty if no transport configuration is defined. This case is intended for applications that may want to change the transport configuration afterwards. :return: None if no transport is configured AND ``reconfigurable`` is False. Otherwise, a functional transport instance is returned. :raises: - :class:`pyuavcan.application.register.MissingRegisterError` if a register is expected but cannot be found. - :class:`pyuavcan.application.register.ValueConversionError` if a register is found but its value cannot be converted to the correct type. .. doctest:: :hide: >>> import tests >>> tests.asyncio_allow_event_loop_access_from_top_level() >>> from pyuavcan.application.register import ValueProxy, Natural16, Natural32 >>> reg = { ... "uavcan.udp.iface": ValueProxy("127.99.0.0"), ... "uavcan.node.id": ValueProxy(Natural16([257])), ... } >>> tr = make_transport(reg) >>> tr UDPTransport('127.99.1.1', local_node_id=257, ...) >>> tr.close() >>> tr = make_transport(reg, reconfigurable=True) # Same but reconfigurable. >>> tr # Wrapped into RedundantTransport. RedundantTransport(UDPTransport('127.99.1.1', local_node_id=257, ...)) >>> tr.close() >>> int(reg["uavcan.udp.mtu"]) # Defaults created automatically to expose all configurables. 1200 >>> int(reg["uavcan.can.mtu"]) 64 >>> reg["uavcan.can.bitrate"].ints [1000000, 4000000] >>> reg = { # Triply-redundant heterogeneous transport: ... "uavcan.udp.iface": ValueProxy("127.99.0.15 127.111.0.15"), # Double UDP transport ... "uavcan.serial.iface": ValueProxy("socket://127.0.0.1:50905"), # Serial transport ... } >>> tr = make_transport(reg) # The node-ID was not set, so the transport is anonymous. >>> tr # doctest: +NORMALIZE_WHITESPACE RedundantTransport(UDPTransport('127.99.0.15', local_node_id=None, ...), UDPTransport('127.111.0.15', local_node_id=None, ...), SerialTransport('socket://127.0.0.1:50905', local_node_id=None, ...)) >>> tr.close() >>> reg = { ... "uavcan.can.iface": ValueProxy("virtual: virtual:"), # Doubly-redundant CAN ... "uavcan.can.mtu": ValueProxy(Natural16([32])), ... "uavcan.can.bitrate": ValueProxy(Natural32([500_000, 2_000_000])), ... "uavcan.node.id": ValueProxy(Natural16([123])), ... } >>> tr = make_transport(reg) >>> tr # doctest: +NORMALIZE_WHITESPACE RedundantTransport(CANTransport(PythonCANMedia('virtual:', mtu=32), local_node_id=123), CANTransport(PythonCANMedia('virtual:', mtu=32), local_node_id=123)) >>> tr.close() >>> reg = { ... "uavcan.udp.iface": ValueProxy("127.99.1.1"), # Per the standard register specs, ... "uavcan.node.id": ValueProxy(Natural16([0xFFFF])), # 0xFFFF means unset/anonymous. ... } >>> tr = make_transport(reg) >>> tr UDPTransport('127.99.1.1', local_node_id=None, ...) >>> tr.close() >>> tr = make_transport({}) >>> tr is None True >>> tr = make_transport({}, reconfigurable=True) >>> tr # Redundant transport with no inferiors. RedundantTransport() """ def init(name: str, default: RelaxedValue) -> ValueProxy: return registers.setdefault("uavcan." + name, ValueProxy(default)) # Per Specification, if uavcan.node.id = 65535, the node-ID is unspecified. node_id: Optional[int] = int(init("node.id", Natural16([0xFFFF]))) # TODO: currently, we raise an error if the node-ID setting exceeds the maximum allowed value for the current # transport, but the spec recommends that we should handle this as if the node-ID was not set at all. if node_id is not None and not (0 <= node_id < 0xFFFF): node_id = None transports = list( itertools.chain(*(f(registers, node_id) for f in _SPECIALIZATIONS))) assert all(isinstance(t, pyuavcan.transport.Transport) for t in transports) if not reconfigurable: if not transports: return None if len(transports) == 1: return transports[0] from pyuavcan.transport.redundant import RedundantTransport red = RedundantTransport() for tr in transports: red.attach_inferior(tr) return red
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)
async def _unittest_slow_node( compiled: typing.List[pyuavcan.dsdl.GeneratedPackageInfo]) -> None: from pyuavcan.application import make_node, make_registry import uavcan.primitive from uavcan.node import Version_1_0, Heartbeat_1_0, GetInfo_1_0, Mode_1_0, Health_1_0 asyncio.get_running_loop().slow_callback_duration = 3.0 assert compiled remote_pres = Presentation(UDPTransport("127.1.1.1")) remote_hb_sub = remote_pres.make_subscriber_with_fixed_subject_id( Heartbeat_1_0) remote_info_cln = remote_pres.make_client_with_fixed_service_id( GetInfo_1_0, 258) trans = RedundantTransport() try: info = GetInfo_1_0.Response( protocol_version=Version_1_0( *pyuavcan.UAVCAN_SPECIFICATION_VERSION), software_version=Version_1_0(*pyuavcan.__version_info__[:2]), name="org.uavcan.pyuavcan.test.node", ) node = make_node(info, make_registry(None, typing.cast(Dict[str, bytes], {})), transport=trans) print("node:", node) assert node.presentation.transport is trans node.start() node.start() # Idempotency # Check port instantiation API for non-fixed-port-ID types. assert "uavcan.pub.optional.id" not in node.registry # Nothing yet. with pytest.raises(KeyError, match=r".*uavcan\.pub\.optional\.id.*"): node.make_publisher(uavcan.primitive.Empty_1_0, "optional") assert 0xFFFF == int( node.registry["uavcan.pub.optional.id"]) # Created automatically! with pytest.raises(TypeError): node.make_publisher(uavcan.primitive.Empty_1_0) # Same but for fixed port-ID types. assert "uavcan.pub.atypical_heartbeat.id" not in node.registry # Nothing yet. port = node.make_publisher(uavcan.node.Heartbeat_1_0, "atypical_heartbeat") assert port.port_id == pyuavcan.dsdl.get_model( uavcan.node.Heartbeat_1_0).fixed_port_id port.close() assert 0xFFFF == int(node.registry["uavcan.pub.atypical_heartbeat.id"] ) # Created automatically! node.registry[ "uavcan.pub.atypical_heartbeat.id"] = 111 # Override the default. port = node.make_publisher(uavcan.node.Heartbeat_1_0, "atypical_heartbeat") assert port.port_id == 111 port.close() node.heartbeat_publisher.priority = pyuavcan.transport.Priority.FAST node.heartbeat_publisher.period = 0.5 node.heartbeat_publisher.mode = Mode_1_0.MAINTENANCE # type: ignore node.heartbeat_publisher.health = Health_1_0.ADVISORY # type: ignore node.heartbeat_publisher.vendor_specific_status_code = 93 with pytest.raises(ValueError): node.heartbeat_publisher.period = 99.0 with pytest.raises(ValueError): node.heartbeat_publisher.vendor_specific_status_code = -299 assert node.heartbeat_publisher.priority == pyuavcan.transport.Priority.FAST assert node.heartbeat_publisher.period == pytest.approx(0.5) assert node.heartbeat_publisher.mode == Mode_1_0.MAINTENANCE assert node.heartbeat_publisher.health == Health_1_0.ADVISORY assert node.heartbeat_publisher.vendor_specific_status_code == 93 assert None is await remote_hb_sub.receive_for(2.0) assert trans.local_node_id is None trans.attach_inferior(UDPTransport("127.1.1.2")) assert trans.local_node_id == 258 for _ in range(2): hb_transfer = await remote_hb_sub.receive_for(2.0) assert hb_transfer is not None hb, transfer = hb_transfer assert transfer.source_node_id == 258 assert transfer.priority == pyuavcan.transport.Priority.FAST assert 1 <= hb.uptime <= 9 assert hb.mode.value == Mode_1_0.MAINTENANCE assert hb.health.value == Health_1_0.ADVISORY assert hb.vendor_specific_status_code == 93 info_transfer = await remote_info_cln.call(GetInfo_1_0.Request()) assert info_transfer is not None resp, transfer = info_transfer assert transfer.source_node_id == 258 assert isinstance(resp, GetInfo_1_0.Response) assert resp.name.tobytes().decode() == "org.uavcan.pyuavcan.test.node" assert resp.protocol_version.major == pyuavcan.UAVCAN_SPECIFICATION_VERSION[ 0] assert resp.software_version.major == pyuavcan.__version_info__[0] trans.detach_inferior(trans.inferiors[0]) assert trans.local_node_id is None assert None is await remote_hb_sub.receive_for(2.0) node.close() node.close() # Idempotency finally: trans.close() remote_pres.close() await asyncio.sleep(1.0) # Let the background tasks terminate.
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
async def _unittest_slow_node( generated_packages: typing.List[pyuavcan.dsdl.GeneratedPackageInfo] ) -> None: from pyuavcan.application import Node from uavcan.node import Version_1_0, Heartbeat_1_0, GetInfo_1_0, Mode_1_0, Health_1_0 asyncio.get_running_loop().slow_callback_duration = 3.0 assert generated_packages remote_pres = Presentation(UDPTransport("127.1.1.1")) remote_hb_sub = remote_pres.make_subscriber_with_fixed_subject_id( Heartbeat_1_0) remote_info_cln = remote_pres.make_client_with_fixed_service_id( GetInfo_1_0, 258) trans = RedundantTransport() pres = Presentation(trans) try: info = GetInfo_1_0.Response( protocol_version=Version_1_0( *pyuavcan.UAVCAN_SPECIFICATION_VERSION), software_version=Version_1_0(*pyuavcan.__version_info__[:2]), name="org.uavcan.pyuavcan.test.node", ) node = Node(pres, info, with_diagnostic_subscriber=True) print("node:", node) assert node.presentation is pres node.start() node.start() # Idempotency node.heartbeat_publisher.priority = pyuavcan.transport.Priority.FAST node.heartbeat_publisher.period = 0.5 node.heartbeat_publisher.mode = Mode_1_0.MAINTENANCE # type: ignore node.heartbeat_publisher.health = Health_1_0.ADVISORY # type: ignore node.heartbeat_publisher.vendor_specific_status_code = 93 with pytest.raises(ValueError): node.heartbeat_publisher.period = 99.0 with pytest.raises(ValueError): node.heartbeat_publisher.vendor_specific_status_code = -299 assert node.heartbeat_publisher.priority == pyuavcan.transport.Priority.FAST assert node.heartbeat_publisher.period == pytest.approx(0.5) assert node.heartbeat_publisher.mode == Mode_1_0.MAINTENANCE assert node.heartbeat_publisher.health == Health_1_0.ADVISORY assert node.heartbeat_publisher.vendor_specific_status_code == 93 assert None is await remote_hb_sub.receive_for(2.0) assert trans.local_node_id is None trans.attach_inferior(UDPTransport("127.1.1.2")) assert trans.local_node_id == 258 for _ in range(2): hb_transfer = await remote_hb_sub.receive_for(2.0) assert hb_transfer is not None hb, transfer = hb_transfer assert transfer.source_node_id == 258 assert transfer.priority == pyuavcan.transport.Priority.FAST assert 1 <= hb.uptime <= 9 assert hb.mode.value == Mode_1_0.MAINTENANCE assert hb.health.value == Health_1_0.ADVISORY assert hb.vendor_specific_status_code == 93 info_transfer = await remote_info_cln.call(GetInfo_1_0.Request()) assert info_transfer is not None resp, transfer = info_transfer assert transfer.source_node_id == 258 assert isinstance(resp, GetInfo_1_0.Response) assert resp.name.tobytes().decode() == "org.uavcan.pyuavcan.test.node" assert resp.protocol_version.major == pyuavcan.UAVCAN_SPECIFICATION_VERSION[ 0] assert resp.software_version.major == pyuavcan.__version_info__[0] trans.detach_inferior(trans.inferiors[0]) assert trans.local_node_id is None assert None is await remote_hb_sub.receive_for(2.0) node.close() node.close() # Idempotency finally: pres.close() remote_pres.close() await asyncio.sleep(1.0) # Let the background tasks terminate.