def __init__(self, name_suffix: str) -> None: from yukon import __version_info__ self._shutdown = False self._node = pyuavcan.application.make_node( NodeInfo( software_version=Version_1_0(*__version_info__[:2]), name=f"org.uavcan.yukon.{name_suffix}", )) if self._node.id is None: raise ValueError( "DCS transport configuration error: node cannot be anonymous") self._head_node_id = int( self._node.registry.setdefault("yukon.dcs.head_node_id", register.Natural16([0xFFFF]))) self._last_head_heartbeat_at = time.monotonic() self._node.heartbeat_publisher.add_pre_heartbeat_handler( self._check_deadman_switch) self._node.make_subscriber(Heartbeat).receive_in_background( self._on_heartbeat) self._node.get_server(ExecuteCommand_1_1).serve_in_background( self._on_execute_command) self._node.start()
async def main() -> None: with make_node(NodeInfo(name="org.uavcan.pyuavcan.demo.plant"), "plant.db") as node: # Expose internal states for diagnostics. node.registry[ "status.saturation"] = lambda: saturation # The register type will be deduced as "bit[1]". # Initialize values from the registry. The temperature is in kelvin because in UAVCAN everything follows SI. # Here, we specify the type explicitly as "real32[1]". If we pass a native float, it would be "real64[1]". temp_environment = float( node.registry.setdefault("model.environment.temperature", register.Real32([292.15]))) temp_plant = temp_environment # Set up the ports. pub_meas = node.make_publisher(uavcan.si.sample.temperature.Scalar_1, "temperature") pub_meas.priority = pyuavcan.transport.Priority.HIGH sub_volt = node.make_subscriber(uavcan.si.unit.voltage.Scalar_1, "voltage") sub_volt.receive_in_background(handle_command) # Run the main loop forever. next_update_at = asyncio.get_running_loop().time() while True: # Publish new measurement and update node health. await pub_meas.publish( uavcan.si.sample.temperature.Scalar_1( timestamp=uavcan.time.SynchronizedTimestamp_1( microsecond=int(time.time() * 1e6)), kelvin=temp_plant, )) node.heartbeat_publisher.health = Health.ADVISORY if saturation else Health.NOMINAL # Sleep until the next iteration. next_update_at += UPDATE_PERIOD await asyncio.sleep(next_update_at - asyncio.get_running_loop().time()) # Update the simulation. temp_plant += heater_voltage * 0.1 * UPDATE_PERIOD # Energy input from the heater. temp_plant -= (temp_plant - temp_environment ) * 0.05 * UPDATE_PERIOD # Dissipation.
async def _unittest_slow_diagnostic_subscriber(compiled: typing.List[ pyuavcan.dsdl.GeneratedPackageInfo], caplog: typing.Any) -> None: from pyuavcan.application import make_node, NodeInfo, diagnostic, make_registry from uavcan.time import SynchronizedTimestamp_1_0 assert compiled asyncio.get_running_loop().slow_callback_duration = 1.0 node = make_node( NodeInfo(), make_registry(None, typing.cast(Dict[str, bytes], {})), transport=LoopbackTransport(2222), ) node.start() pub = node.make_publisher(diagnostic.Record) diagnostic.DiagnosticSubscriber(node) caplog.clear() await pub.publish( diagnostic.Record( timestamp=SynchronizedTimestamp_1_0(123456789), severity=diagnostic.Severity(diagnostic.Severity.INFO), text="Hello world!", )) await asyncio.sleep(1.0) print("Captured log records:") for lr in caplog.records: print(" ", lr) assert isinstance(lr, logging.LogRecord) pat = r"uavcan\.diagnostic\.Record: node=2222 severity=2 ts_sync=123\.456789 ts_local=\S+:\nHello world!" if lr.levelno == logging.INFO and re.match(pat, lr.message): break else: assert False, "Expected log message not captured" pub.close() node.close() await asyncio.sleep(1.0) # Let the background tasks terminate.
async def _unittest_slow_plug_and_play_centralized( compiled: typing.List[pyuavcan.dsdl.GeneratedPackageInfo], mtu: int ) -> None: from pyuavcan.application import make_node, NodeInfo from pyuavcan.application.plug_and_play import CentralizedAllocator, Allocatee assert compiled asyncio.get_running_loop().slow_callback_duration = 5.0 peers: typing.Set[MockMedia] = set() trans_client = CANTransport(MockMedia(peers, mtu, 1), None) node_server = make_node( NodeInfo(unique_id=_uid("deadbeefdeadbeefdeadbeefdeadbeef")), transport=CANTransport(MockMedia(peers, mtu, 1), 123), ) node_server.start() cln_a = Allocatee(trans_client, _uid("00112233445566778899aabbccddeeff"), 42) assert cln_a.get_result() is None await asyncio.sleep(2.0) assert cln_a.get_result() is None # Nope, no response. try: _TABLE.unlink() except FileNotFoundError: pass with pytest.raises(ValueError, match=".*anonymous.*"): CentralizedAllocator(make_node(NodeInfo(), transport=trans_client), _TABLE) allocator = CentralizedAllocator(node_server, _TABLE) allocator.register_node(41, None) allocator.register_node(41, _uid("00000000000000000000000000000001")) # Overwrites allocator.register_node(42, _uid("00000000000000000000000000000002")) allocator.register_node(42, None) # Does not overwrite allocator.register_node(43, _uid("0000000000000000000000000000000F")) allocator.register_node(43, _uid("00000000000000000000000000000003")) # Overwrites allocator.register_node(43, None) # Does not overwrite use_v2 = mtu > cln_a._MTU_THRESHOLD # pylint: disable=protected-access await asyncio.sleep(2.0) assert cln_a.get_result() == (44 if use_v2 else 125) # Another request. cln_b = Allocatee(trans_client, _uid("aabbccddeeff00112233445566778899")) assert cln_b.get_result() is None await asyncio.sleep(2.0) assert cln_b.get_result() == (125 if use_v2 else 124) # Re-request A and make sure we get the same response. cln_a = Allocatee(trans_client, _uid("00112233445566778899aabbccddeeff"), 42) assert cln_a.get_result() is None await asyncio.sleep(2.0) assert cln_a.get_result() == (44 if use_v2 else 125) # C should be served from the manually added entries above. cln_c = Allocatee(trans_client, _uid("00000000000000000000000000000003")) assert cln_c.get_result() is None await asyncio.sleep(2.0) assert cln_c.get_result() == 43 # This one requires no allocation because the transport is not anonymous. cln_d = Allocatee(node_server.presentation, _uid("00000000000000000000000000000009"), 100) assert cln_d.get_result() == 123 await asyncio.sleep(2.0) assert cln_d.get_result() == 123 # No change. # More test coverage needed. # Finalization. cln_a.close() cln_b.close() cln_c.close() cln_d.close() trans_client.close() node_server.close() await asyncio.sleep(1.0) # Let the tasks finalize properly.
async def _unittest_file( compiled: typing.List[pyuavcan.dsdl.GeneratedPackageInfo]) -> None: from pyuavcan.application import make_node, NodeInfo from pyuavcan.transport.udp import UDPTransport from pyuavcan.application.file import FileClient, FileServer, Error assert compiled asyncio.get_running_loop().slow_callback_duration = 3.0 root_a = mkdtemp(".file", "a.") root_b = mkdtemp(".file", "b.") srv_node = make_node( NodeInfo(name="org.uavcan.pyuavcan.test.file.server"), transport=UDPTransport("127.63.0.0", 222, service_transfer_multiplier=2), ) cln_node = make_node( NodeInfo(name="org.uavcan.pyuavcan.test.file.client"), transport=UDPTransport("127.63.0.0", 223, service_transfer_multiplier=2), ) try: srv_node.start() file_server = FileServer(srv_node, [root_a, root_b]) assert (Path(root_a), Path("abc")) == file_server.locate(Path("abc")) assert [] == list(file_server.glob("*")) cln_node.start() cln = FileClient(cln_node, 222) async def ls(path: str) -> typing.List[str]: out: typing.List[str] = [] async for e in cln.list(path): out.append(e) return out assert [] == await ls("") assert [] == await ls("nonexistent/directory") assert (await cln.get_info("none")).error.value == Error.NOT_FOUND assert 0 == await cln.touch("a/foo/x") assert 0 == await cln.touch("a/foo/y") assert 0 == await cln.touch("b") assert ["foo"] == await ls("a") # Make sure files are created. assert [ (file_server.roots[0], Path("a/foo/x")), (file_server.roots[0], Path("a/foo/y")), ] == list(sorted(file_server.glob("a/foo/*"))) assert await cln.read("a/foo/x") == b"" assert await cln.read( "/a/foo/x") == b"" # Slash or no slash makes no difference. assert await cln.read("a/foo/z") == Error.NOT_FOUND assert (await cln.get_info("a/foo/z")).error.value == Error.NOT_FOUND # Write non-existent file assert await cln.write("a/foo/z", bytes(range(200)) * 3) == Error.NOT_FOUND # Write into empty file assert await cln.write("a/foo/x", bytes(range(200)) * 3) == 0 assert await cln.read("a/foo/x") == bytes(range(200)) * 3 assert (await cln.get_info("a/foo/x")).size == 600 # Truncation -- this write is shorter hundred = bytes(x ^ 0xFF for x in range(100)) assert await cln.write("a/foo/x", hundred * 4) == 0 assert (await cln.get_info("a/foo/x")).size == 400 assert await cln.read("a/foo/x") == (hundred * 4) assert (await cln.get_info("a/foo/x")).size == 400 # Fill in the middle without truncation ref = bytearray(hundred * 4) for i in range(100): ref[i + 100] = 0x55 assert len(ref) == 400 assert (await cln.get_info("a/foo/x")).size == 400 assert await cln.write("a/foo/x", b"\x55" * 100, offset=100, truncate=False) == 0 assert (await cln.get_info("a/foo/x")).size == 400 assert await cln.read("a/foo/x") == ref # Fill in the middle with truncation assert await cln.write("a/foo/x", b"\xAA" * 50, offset=50) == 0 assert (await cln.get_info("a/foo/x")).size == 100 assert await cln.read("a/foo/x") == hundred[:50] + b"\xAA" * 50 # Directories info = await cln.get_info("a/foo") print("a/foo:", info) assert info.error.value == 0 assert info.is_writeable assert info.is_readable assert not info.is_file_not_directory assert not info.is_link assert (await cln.get_info("a/foo/nothing")).error.value == Error.NOT_FOUND assert await cln.write("a/foo", b"123") in (Error.IS_DIRECTORY, Error.ACCESS_DENIED ) # Windows compatibility # Removal assert (await cln.remove("a/foo/z")) == Error.NOT_FOUND assert (await cln.remove("a/foo/x")) == 0 assert (await cln.touch("a/foo/x")) == 0 # Put it back assert (await cln.remove("a/foo/")) == 0 # Removed assert (await cln.remove("a/foo/")) == Error.NOT_FOUND # Not found # Copy assert (await cln.touch("r/a")) == 0 assert (await cln.touch("r/b/0")) == 0 assert (await cln.touch("r/b/1")) == 0 assert not (await cln.get_info("r/b")).is_file_not_directory assert ["a", "b"] == await ls("r") assert (await cln.copy("r/b", "r/c")) == 0 assert ["a", "b", "c"] == await ls("r") assert (await cln.copy("r/a", "r/c")) != 0 # Overwrite not enabled assert ["a", "b", "c"] == await ls("r") assert not (await cln.get_info("r/c")).is_file_not_directory assert (await cln.copy("/r/a", "r/c", overwrite=True)) == 0 assert (await cln.get_info("r/c")).is_file_not_directory # Move assert ["a", "b", "c"] == await ls("r") assert (await cln.move("/r/a", "r/c")) != 0 # Overwrite not enabled assert (await cln.move("/r/a", "r/c", overwrite=True)) == 0 assert ["b", "c"] == await ls("r") assert (await cln.move("/r/a", "r/c", overwrite=True)) == Error.NOT_FOUND assert ["b", "c"] == await ls("r") # Access protected files if sys.platform.startswith("linux"): # pragma: no branch file_server.roots.append(Path("/")) info = await cln.get_info("dev/null") print("/dev/null:", info) assert info.error.value == 0 assert not info.is_link assert info.is_writeable assert info.is_file_not_directory info = await cln.get_info("/bin/sh") print("/bin/sh:", info) assert info.error.value == 0 assert not info.is_writeable assert info.is_file_not_directory assert await cln.read("/dev/null", size=100) == b"" # Read less than requested assert await cln.read( "/dev/zero", size=100) == b"\x00" * 256 # Read more than requested assert await cln.write("bin/sh", b"123") == Error.ACCESS_DENIED file_server.roots.pop(-1) finally: srv_node.close() cln_node.close() await asyncio.sleep(1.0) shutil.rmtree(root_a, ignore_errors=True) shutil.rmtree(root_b, ignore_errors=True)
async def _unittest_slow_node_tracker( compiled: typing.List[pyuavcan.dsdl.GeneratedPackageInfo]) -> None: from . import get_transport from uavcan.node import GetInfo_1_0 from pyuavcan.application import make_node, NodeInfo from pyuavcan.application.node_tracker import NodeTracker, Entry assert compiled asyncio.get_running_loop().slow_callback_duration = 3.0 n_a = make_node(NodeInfo(name="org.uavcan.pyuavcan.test.node_tracker.a"), transport=get_transport(0xA)) n_b = make_node(NodeInfo(name="org.uavcan.pyuavcan.test.node_tracker.b"), transport=get_transport(0xB)) n_c = make_node(NodeInfo(name="org.uavcan.pyuavcan.test.node_tracker.c"), transport=get_transport(0xC)) n_trk = make_node( NodeInfo(name="org.uavcan.pyuavcan.test.node_tracker.trk"), transport=get_transport(None)) try: last_update_args: typing.List[typing.Tuple[ int, typing.Optional[Entry], typing.Optional[Entry]]] = [] def simple_handler(node_id: int, old: typing.Optional[Entry], new: typing.Optional[Entry]) -> None: last_update_args.append((node_id, old, new)) trk = NodeTracker(n_trk) assert not trk.registry assert pytest.approx( trk.get_info_timeout) == trk.DEFAULT_GET_INFO_TIMEOUT assert trk.get_info_attempts == trk.DEFAULT_GET_INFO_ATTEMPTS # Override the defaults to simplify and speed-up testing. trk.get_info_timeout = 1.0 trk.get_info_attempts = 2 assert pytest.approx(trk.get_info_timeout) == 1.0 assert trk.get_info_attempts == 2 trk.add_update_handler(simple_handler) n_trk.start() n_trk.start() # Idempotency. await asyncio.sleep(9) assert not last_update_args assert not trk.registry # Bring the first node online and make sure it is detected and reported. n_a.heartbeat_publisher.vendor_specific_status_code = 0xDE n_a.start() await asyncio.sleep(9) assert len(last_update_args) == 1 assert last_update_args[0][0] == 0xA assert last_update_args[0][1] is None assert last_update_args[0][2] is not None assert last_update_args[0][2].heartbeat.uptime == 0 assert last_update_args[0][ 2].heartbeat.vendor_specific_status_code == 0xDE last_update_args.clear() assert list(trk.registry.keys()) == [0xA] assert 30 >= trk.registry[0xA].heartbeat.uptime >= 2 assert trk.registry[0xA].heartbeat.vendor_specific_status_code == 0xDE assert trk.registry[0xA].info is None # Bring the second node online and make sure it is detected and reported. n_b.heartbeat_publisher.vendor_specific_status_code = 0xBE n_b.start() await asyncio.sleep(9) assert len(last_update_args) == 1 assert last_update_args[0][0] == 0xB assert last_update_args[0][1] is None assert last_update_args[0][2] is not None assert last_update_args[0][2].heartbeat.uptime == 0 assert last_update_args[0][ 2].heartbeat.vendor_specific_status_code == 0xBE last_update_args.clear() assert list(trk.registry.keys()) == [0xA, 0xB] assert 60 >= trk.registry[0xA].heartbeat.uptime >= 4 assert trk.registry[0xA].heartbeat.vendor_specific_status_code == 0xDE assert trk.registry[0xA].info is None assert 30 >= trk.registry[0xB].heartbeat.uptime >= 2 assert trk.registry[0xB].heartbeat.vendor_specific_status_code == 0xBE assert trk.registry[0xB].info is None await asyncio.sleep(9) assert not last_update_args assert list(trk.registry.keys()) == [0xA, 0xB] assert 90 >= trk.registry[0xA].heartbeat.uptime >= 6 assert trk.registry[0xA].heartbeat.vendor_specific_status_code == 0xDE assert trk.registry[0xA].info is None assert 60 >= trk.registry[0xB].heartbeat.uptime >= 4 assert trk.registry[0xB].heartbeat.vendor_specific_status_code == 0xBE assert trk.registry[0xB].info is None # Create a new tracker, this time with a valid node-ID, and make sure node info is requested. # We are going to need a new handler for this. num_events_a = 0 num_events_b = 0 num_events_c = 0 def validating_handler(node_id: int, old: typing.Optional[Entry], new: typing.Optional[Entry]) -> None: nonlocal num_events_a, num_events_b, num_events_c _logger.info("VALIDATING HANDLER %s %s %s", node_id, old, new) if node_id == 0xA: if num_events_a == 0: # First detection assert old is None assert new is not None assert new.heartbeat.vendor_specific_status_code == 0xDE assert new.info is None elif num_events_a == 1: # Get info received assert old is not None assert new is not None assert old.heartbeat.vendor_specific_status_code == 0xDE assert new.heartbeat.vendor_specific_status_code == 0xDE assert old.info is None assert new.info is not None assert new.info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.a" elif num_events_a == 2: # Restart detected assert old is not None assert new is not None assert old.heartbeat.vendor_specific_status_code == 0xDE assert new.heartbeat.vendor_specific_status_code == 0xFE assert old.info is not None assert new.info is None elif num_events_a == 3: # Get info after restart received assert old is not None assert new is not None assert old.heartbeat.vendor_specific_status_code == 0xFE assert new.heartbeat.vendor_specific_status_code == 0xFE assert old.info is None assert new.info is not None assert new.info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.a" elif num_events_a == 4: # Offline assert old is not None assert new is None assert old.heartbeat.vendor_specific_status_code == 0xFE assert old.info is not None else: assert False num_events_a += 1 elif node_id == 0xB: if num_events_b == 0: assert old is None assert new is not None assert new.heartbeat.vendor_specific_status_code == 0xBE assert new.info is None elif num_events_b == 1: assert old is not None assert new is not None assert old.heartbeat.vendor_specific_status_code == 0xBE assert new.heartbeat.vendor_specific_status_code == 0xBE assert old.info is None assert new.info is not None assert new.info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.b" elif num_events_b == 2: assert old is not None assert new is None assert old.heartbeat.vendor_specific_status_code == 0xBE assert old.info is not None else: assert False num_events_b += 1 elif node_id == 0xC: if num_events_c == 0: assert old is None assert new is not None assert new.heartbeat.vendor_specific_status_code == 0xF0 assert new.info is None elif num_events_c == 1: assert old is not None assert new is None assert old.heartbeat.vendor_specific_status_code == 0xF0 assert old.info is None else: assert False num_events_c += 1 else: assert False n_trk.close() n_trk.close() # Idempotency n_trk = make_node(n_trk.info, transport=get_transport(0xDD)) n_trk.start() trk = NodeTracker(n_trk) trk.add_update_handler(validating_handler) trk.get_info_timeout = 1.0 trk.get_info_attempts = 2 assert pytest.approx(trk.get_info_timeout) == 1.0 assert trk.get_info_attempts == 2 await asyncio.sleep(9) assert num_events_a == 2 assert num_events_b == 2 assert num_events_c == 0 assert list(trk.registry.keys()) == [0xA, 0xB] assert 60 >= trk.registry[0xA].heartbeat.uptime >= 8 assert trk.registry[0xA].heartbeat.vendor_specific_status_code == 0xDE assert trk.registry[0xA].info is not None assert trk.registry[0xA].info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.a" assert 60 >= trk.registry[0xB].heartbeat.uptime >= 6 assert trk.registry[0xB].heartbeat.vendor_specific_status_code == 0xBE assert trk.registry[0xB].info is not None assert trk.registry[0xB].info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.b" # Node B goes offline. n_b.close() await asyncio.sleep(9) assert num_events_a == 2 assert num_events_b == 3 assert num_events_c == 0 assert list(trk.registry.keys()) == [0xA] assert 90 >= trk.registry[0xA].heartbeat.uptime >= 12 assert trk.registry[0xA].heartbeat.vendor_specific_status_code == 0xDE assert trk.registry[0xA].info is not None assert trk.registry[0xA].info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.a" # Node C appears online. It does not respond to GetInfo. n_c.heartbeat_publisher.vendor_specific_status_code = 0xF0 n_c.start() # To make it not respond to GetInfo, get under the hood and break the transport session for this RPC-service. get_info_service_id = pyuavcan.dsdl.get_fixed_port_id(GetInfo_1_0) assert get_info_service_id for ses in n_c.presentation.transport.input_sessions: ds = ses.specifier.data_specifier if isinstance(ds, pyuavcan.transport.ServiceDataSpecifier ) and ds.service_id == get_info_service_id: ses.close() await asyncio.sleep(9) assert num_events_a == 2 assert num_events_b == 3 assert num_events_c == 1 assert list(trk.registry.keys()) == [0xA, 0xC] assert 180 >= trk.registry[0xA].heartbeat.uptime >= 17 assert trk.registry[0xA].heartbeat.vendor_specific_status_code == 0xDE assert trk.registry[0xA].info is not None assert trk.registry[0xA].info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.a" assert 30 >= trk.registry[0xC].heartbeat.uptime >= 5 assert trk.registry[0xC].heartbeat.vendor_specific_status_code == 0xF0 assert trk.registry[0xC].info is None # Node A is restarted. Node C goes offline. n_a.close() n_c.close() n_a = make_node( NodeInfo(name="org.uavcan.pyuavcan.test.node_tracker.a"), transport=get_transport(0xA)) n_a.heartbeat_publisher.vendor_specific_status_code = 0xFE n_a.start() await asyncio.sleep(9) assert num_events_a == 4 # Two extra events: node restart detection, then get info reception. assert num_events_b == 3 assert num_events_c == 2 assert list(trk.registry.keys()) == [0xA] assert 30 >= trk.registry[0xA].heartbeat.uptime >= 5 assert trk.registry[0xA].heartbeat.vendor_specific_status_code == 0xFE assert trk.registry[0xA].info is not None assert trk.registry[0xA].info.name.tobytes().decode( ) == "org.uavcan.pyuavcan.test.node_tracker.a" # Node A goes offline. No online nodes are left standing. n_a.close() await asyncio.sleep(9) assert num_events_a == 5 assert num_events_b == 3 assert num_events_c == 2 assert not trk.registry finally: for p in [n_a, n_b, n_c, n_trk]: p.close() await asyncio.sleep( 1 ) # Let all pending tasks finalize properly to avoid stack traces in the output.