async def _unittest_can_pythoncan_socketcan() -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 media_a = PythonCANMedia("socketcan:vcan2", 0, 8) media_b = PythonCANMedia("socketcan:vcan2", 0, 64) rx_a: typing.List[typing.Tuple[Timestamp, Envelope]] = [] rx_b: typing.List[typing.Tuple[Timestamp, Envelope]] = [] def on_rx_a(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None: nonlocal rx_a rx_a += list(frames) def on_rx_b(frames: typing.Iterable[typing.Tuple[Timestamp, Envelope]]) -> None: nonlocal rx_b rx_b += list(frames) media_a.start(on_rx_a, no_automatic_retransmission=False) media_b.start(on_rx_b, no_automatic_retransmission=False) ts_begin = Timestamp.now() await media_a.send( [ Envelope(DataFrame(FrameFormat.EXTENDED, 0xBADC0FE, bytearray(b"123")), loopback=True), Envelope(DataFrame(FrameFormat.EXTENDED, 0x12345678, bytearray(b"456")), loopback=False), ], asyncio.get_event_loop().time() + 1.0, ) await asyncio.sleep(1.0) ts_end = Timestamp.now() assert len(rx_b) == 2 assert ts_begin.monotonic_ns <= rx_b[0][0].monotonic_ns <= ts_end.monotonic_ns assert ts_begin.monotonic_ns <= rx_b[1][0].monotonic_ns <= ts_end.monotonic_ns assert ts_begin.system_ns <= rx_b[0][0].system_ns <= ts_end.system_ns assert ts_begin.system_ns <= rx_b[1][0].system_ns <= ts_end.system_ns assert not rx_b[0][1].loopback assert not rx_b[1][1].loopback assert rx_b[0][1].frame.identifier == 0xBADC0FE assert rx_b[1][1].frame.identifier == 0x12345678 assert rx_b[0][1].frame.data == b"123" assert rx_b[1][1].frame.data == b"456" assert len(rx_a) == 1 assert ts_begin.monotonic_ns <= rx_a[0][0].monotonic_ns <= ts_end.monotonic_ns assert ts_begin.system_ns <= rx_a[0][0].system_ns <= ts_end.system_ns assert rx_a[0][1].loopback assert rx_a[0][1].frame.identifier == 0xBADC0FE assert rx_a[0][1].frame.data == b"123" media_a.close() media_b.close() media_a.close() # Ensure idempotency. media_b.close()
def _unittest_issue_198() -> None: source_node_id = 88 transfer_id_timeout_ns = 900 def mk_frame( padded_payload: bytes | str, transfer_id: int, start_of_transfer: bool, end_of_transfer: bool, toggle_bit: bool, ) -> UAVCANFrame: return UAVCANFrame( identifier=0xBADC0FE, padded_payload=memoryview(padded_payload if isinstance( padded_payload, bytes) else padded_payload.encode()), transfer_id=transfer_id, start_of_transfer=start_of_transfer, end_of_transfer=end_of_transfer, toggle_bit=toggle_bit, ) rx = TransferReassembler(source_node_id, 50) # First, ensure that the reassembler is initialized, by feeding it a valid transfer at least once. assert rx.process_frame( timestamp=Timestamp(system_ns=0, monotonic_ns=1000), priority=pyuavcan.transport.Priority.SLOW, frame=mk_frame("123", 0, True, True, True), transfer_id_timeout_ns=transfer_id_timeout_ns, ) == TransferFrom( timestamp=Timestamp(system_ns=0, monotonic_ns=1000), priority=pyuavcan.transport.Priority.SLOW, transfer_id=0, fragmented_payload=[ memoryview(x if isinstance(x, (bytes, memoryview)) else x.encode()) for x in ["123"] ], source_node_id=source_node_id, ) # Next, feed the last frame of another transfer whose TID/TOG match the expected state of the reassembler. # This should be recognized as a CRC error. assert (rx.process_frame( timestamp=Timestamp(system_ns=0, monotonic_ns=1000), priority=pyuavcan.transport.Priority.SLOW, frame=mk_frame("456", 1, False, True, True), transfer_id_timeout_ns=transfer_id_timeout_ns, ) == TransferReassemblyErrorID.TRANSFER_CRC_MISMATCH)
async def _emit(self, header_payload_pairs: typing.Sequence[typing.Tuple[ memoryview, memoryview]], monotonic_deadline: float) -> typing.Optional[Timestamp]: """ Returns the transmission timestamp of the first frame (which is the transfer timestamp) on success. Returns None if at least one frame could not be transmitted. """ ts: typing.Optional[Timestamp] = None loop = asyncio.get_running_loop() for index, (header, payload) in enumerate(header_payload_pairs): try: # TODO: concatenation is inefficient. Use vectorized IO via sendmsg() instead! await asyncio.wait_for( loop.sock_sendall(self._sock, b"".join((header, payload))), timeout=monotonic_deadline - loop.time(), ) # TODO: use socket timestamping when running on Linux (Windows does not support timestamping). # Depending on the chosen approach, timestamping on Linux may require us to launch a new thread # reading from the socket's error message queue and then matching the returned frames with a # pending loopback registry, kind of like it's done with CAN. ts = ts or Timestamp.now() except (asyncio.TimeoutError, asyncio.CancelledError): self._statistics.drops += len(header_payload_pairs) - index return None except Exception as ex: if _IGNORE_OS_ERROR_ON_SEND and isinstance( ex, OSError) and self._sock.fileno() >= 0: # Windows compatibility workaround -- if there are no registered multicast receivers on the # loopback interface, send() may raise WinError 1231 or 10051. This error shall be suppressed. _logger.debug( "%r: Socket send error ignored (the likely cause is that there are no known receivers " "on the other end of the link): %r", self, ex, ) # To suppress the error properly, we have to pretend that the data was actually transmitted, # so we populate the timestamp with a phony value anyway. ts = ts or Timestamp.now() else: self._statistics.errors += 1 raise self._statistics.frames += 1 self._statistics.payload_bytes += len(payload) return ts
def _unittest_validate_and_finalize_transfer() -> None: ts = Timestamp.now() prio = Priority.FAST tid = 888888888 src_nid = 1234 def mk_transfer(fp: typing.Sequence[bytes]) -> TransferFrom: return TransferFrom( timestamp=ts, priority=prio, transfer_id=tid, fragmented_payload=list(map(memoryview, fp)), source_node_id=src_nid, ) def call(fp: typing.Sequence[bytes]) -> typing.Optional[TransferFrom]: return _validate_and_finalize_transfer( timestamp=ts, priority=prio, transfer_id=tid, frame_payloads=list(map(memoryview, fp)), source_node_id=src_nid, ) assert call([b""]) == mk_transfer([b""]) assert call([b"hello world"]) == mk_transfer([b"hello world"]) assert call([ b"hello world", b"0123456789", TransferCRC.new(b"hello world", b"0123456789").value_as_bytes ]) == mk_transfer([b"hello world", b"0123456789"]) assert call([b"hello world", b"0123456789"]) is None # no CRC
def __init__( self, source_node_id: int, extent_bytes: int, on_error_callback: typing.Callable[[TransferReassembler.Error], None], ): """ :param source_node_id: The remote node-ID whose transfers this instance will be listening for. Anonymous transfers cannot be multi-frame transfers, so they are to be accepted as-is without any reassembly activities. :param extent_bytes: The maximum number of payload bytes per transfer. Payload that exceeds this size limit may be implicitly truncated (in the Specification this behavior is described as "implicit truncation rule"). This value can be derived from the corresponding DSDL definition. :param on_error_callback: The callback is invoked whenever an error is detected. This is intended for diagnostic purposes only; the error information is not actionable. The error is logged by the caller at the DEBUG verbosity level together with reassembly context info. """ # Constant configuration. self._source_node_id = int(source_node_id) self._extent_bytes = int(extent_bytes) self._on_error_callback = on_error_callback if self._source_node_id < 0 or self._extent_bytes < 0 or not callable( self._on_error_callback): raise ValueError("Invalid parameters") # Internal state. self._payloads: typing.List[memoryview] = [ ] # Payload fragments from the received frames. self._max_index: typing.Optional[ int] = None # Max frame index in transfer, None if unknown. self._timestamp = Timestamp(0, 0) # First frame timestamp. self._transfer_id = 0 # Transfer-ID of the current transfer.
def _unittest_transfer_reassembler_anonymous() -> None: ts = Timestamp.now() prio = Priority.LOW assert TransferReassembler.construct_anonymous_transfer( ts, Frame(priority=prio, transfer_id=123456, index=0, end_of_transfer=True, payload=memoryview(b"abcdef")), ) == TransferFrom(timestamp=ts, priority=prio, transfer_id=123456, fragmented_payload=[memoryview(b"abcdef")], source_node_id=None) assert (TransferReassembler.construct_anonymous_transfer( ts, Frame(priority=prio, transfer_id=123456, index=1, end_of_transfer=True, payload=memoryview(b"abcdef")), ) is None) assert (TransferReassembler.construct_anonymous_transfer( ts, Frame(priority=prio, transfer_id=123456, index=0, end_of_transfer=False, payload=memoryview(b"abcdef")), ) is None)
def _unittest_frame_compile_service() -> None: from pyuavcan.transport import Priority, ServiceDataSpecifier, Timestamp f = SerialFrame(timestamp=Timestamp.now(), priority=Priority.FAST, source_node_id=SerialFrame.FRAME_DELIMITER_BYTE, destination_node_id=None, data_specifier=ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE), transfer_id=1234567890123456789, index=1234567, end_of_transfer=False, payload=memoryview(b'')) buffer = bytearray(0 for _ in range(50)) mv = f.compile_into(buffer) assert mv[0] == mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE segment_cobs = bytes(mv[1:-1]) assert SerialFrame.FRAME_DELIMITER_BYTE not in segment_cobs segment = cobs.decode(segment_cobs) # Header validation assert segment[0] == _VERSION assert segment[1] == int(Priority.FAST) assert (segment[2], segment[3]) == (SerialFrame.FRAME_DELIMITER_BYTE, 0) assert (segment[4], segment[5]) == (0xFF, 0xFF) assert segment[6:8] == ((1 << 15) | (1 << 14) | 123).to_bytes(2, 'little') assert segment[8:16] == b'\x00' * 8 assert segment[16:24] == 1234567890123456789 .to_bytes(8, 'little') assert segment[24:28] == 1234567 .to_bytes(4, 'little') # Header CRC here # CRC validation assert segment[32:] == pyuavcan.transport.commons.crc.CRC32C.new(f.payload).value_as_bytes
def callback(lls: LinkLayerCapture) -> None: nonlocal ts_last now = Timestamp.now() assert ts_last.monotonic_ns <= lls.timestamp.monotonic_ns <= now.monotonic_ns assert ts_last.system_ns <= lls.timestamp.system_ns <= now.system_ns ts_last = lls.timestamp sniffs.append(lls.packet)
def _unittest_transfer_reassembler_anonymous() -> None: from pyuavcan.transport import Timestamp, Priority, TransferFrom ts = Timestamp.now() prio = Priority.LOW assert TransferReassembler.construct_anonymous_transfer( Frame(timestamp=ts, priority=prio, transfer_id=123456, index=0, end_of_transfer=True, payload=memoryview(b'abcdef'))) == TransferFrom( timestamp=ts, priority=prio, transfer_id=123456, fragmented_payload=[memoryview(b'abcdef')], source_node_id=None) assert TransferReassembler.construct_anonymous_transfer( Frame(timestamp=ts, priority=prio, transfer_id=123456, index=1, end_of_transfer=True, payload=memoryview(b'abcdef'))) is None assert TransferReassembler.construct_anonymous_transfer( Frame(timestamp=ts, priority=prio, transfer_id=123456, index=0, end_of_transfer=False, payload=memoryview(b'abcdef'))) is None
def proxy(_: object, header: ctypes.Structure, packet: typing.Any) -> None: # Parse the header, extract the timestamp and the packet length. header = header.contents ts_ns = (header.ts.tv_sec * 1_000_000 + header.ts.tv_usec) * 1000 ts = Timestamp(system_ns=ts_ns, monotonic_ns=time.monotonic_ns()) length, real_length = header.caplen, header.len _logger.debug("%r: CAPTURED PACKET ts=%s dev=%r len=%d bytes", self, ts, name, length) if real_length != length: # In theory, this should never occur because we use a huge capture buffer. # On Windows, however, when using Npcap v0.96, the captured length is (always?) reported to be # 32 bytes shorter than the real length, despite the fact that the packet is not truncated. _logger.debug( "%r: Length mismatch in a packet captured from %r: real %r bytes, captured %r bytes", self, name, real_length, length, ) # Create a copy of the payload. This is required per the libpcap API contract -- it says that the # memory is invalidated upon return from the callback. packet = memoryview(ctypes.cast(packet, ctypes.POINTER(ctypes.c_ubyte * length))[0]).tobytes() llp = decoder(memoryview(packet)) if llp is None: if _logger.isEnabledFor(logging.INFO): _logger.info( "%r: Link-layer packet of %d bytes captured from %r at %s could not be parsed. " "The header is: %s", self, len(packet), name, ts, packet[:32].hex(), ) else: self._callback(LinkLayerCapture(timestamp=ts, packet=llp, device_name=name))
async def send(self, frames: typing.Iterable[Envelope], monotonic_deadline: float) -> int: num_sent = 0 loopback: typing.List[typing.Tuple[Timestamp, Envelope]] = [] for f in frames: if self._closed: raise ResourceClosedError(repr(self)) message = can.Message( arbitration_id=f.frame.identifier, is_extended_id=(f.frame.format == FrameFormat.EXTENDED), data=f.frame.data, is_fd=self._is_fd, ) try: await self._loop.run_in_executor( self._background_executor, functools.partial(self._bus.send, message, timeout=monotonic_deadline - self._loop.time()), ) except (asyncio.TimeoutError, can.CanError ): # CanError is also used to report timeouts (weird). break else: num_sent += 1 if f.loopback: loopback.append((Timestamp.now(), f)) if loopback: self.loop.call_soon(self._invoke_rx_handler, loopback) return num_sent
async def send(self, frames: typing.Iterable[Envelope], monotonic_deadline: float) -> int: del monotonic_deadline # Unused if self._closed: raise pyuavcan.transport.ResourceClosedError if self._raise_on_send_once: self._raise_on_send_once, ex = None, self._raise_on_send_once assert isinstance(ex, Exception) raise ex frames = list(frames) assert len(frames) > 0, "Interface constraint violation: empty transmission set" assert min(map(lambda x: len(x.frame.data), frames)) >= 1, "CAN frames with empty payload are not valid" # The media interface spec says that it is guaranteed that the CAN ID is the same across the set; enforce this. assert len(set(map(lambda x: x.frame.identifier, frames))) == 1, "Interface constraint violation: nonuniform ID" timestamp = Timestamp.now() # Broadcast across the virtual bus we're emulating here. for p in self._peers: if p is not self: # Unconditionally clear the loopback flag because for the other side these are # regular received frames, not loopback frames. p._receive( # pylint: disable=protected-access (timestamp, Envelope(f.frame, loopback=False)) for f in frames ) # Simple loopback emulation with acceptance filtering. self._receive((timestamp, f) for f in frames if f.loopback) return len(frames)
def __init__(self, source_node_id: int, extent_bytes: int): self._source_node_id = int(source_node_id) self._timestamp = Timestamp(0, 0) self._transfer_id = 0 self._toggle_bit = False self._max_payload_size_bytes_with_crc = int(extent_bytes) + TRANSFER_CRC_LENGTH_BYTES self._crc = pyuavcan.transport.commons.crc.CRC16CCITT() self._payload_truncated = False self._fragmented_payload: typing.List[memoryview] = []
def proc(monotonic_ns: int, frame: UAVCANFrame) -> typing.Union[None, TransferReassemblyErrorID, TransferFrom]: away = rx.process_frame( timestamp=Timestamp(system_ns=0, monotonic_ns=monotonic_ns), priority=priority, frame=frame, transfer_id_timeout_ns=transfer_id_timeout_ns, ) assert away is None or isinstance(away, (TransferReassemblyErrorID, TransferFrom)) return away
def inject_received( self, frames: typing.Iterable[typing.Union[Envelope, DataFrame]]) -> None: timestamp = Timestamp.now() self._receive(( timestamp, (f if isinstance(f, Envelope ) else Envelope(frame=f, loopback=False)), ) for f in frames)
def sniff_sniff(ts: Timestamp, pack: RawPacket) -> None: nonlocal ts_last now = Timestamp.now() assert ts_last.monotonic_ns <= ts.monotonic_ns <= now.monotonic_ns assert ts_last.system_ns <= ts.system_ns <= now.system_ns ts_last = ts # Make sure that all traffic from foreign networks is filtered out by the sniffer. assert (int(pack.ip_header.source) & 0x_FFFF_0000) == (int(fac.local_ip_address) & 0x_FFFF_0000) sniffs.append(pack)
def sniff_sniff(cap: LinkLayerCapture) -> None: nonlocal ts_last now = Timestamp.now() assert ts_last.monotonic_ns <= cap.timestamp.monotonic_ns <= now.monotonic_ns assert ts_last.system_ns <= cap.timestamp.system_ns <= now.system_ns ts_last = cap.timestamp # Make sure that all traffic from foreign networks is filtered out by the sniffer. assert (int(parse_ip(cap.packet).source_destination[0]) & 0x_FFFF_0000) == (int(fac.local_ip_address) & 0x_FFFF_0000) sniffs.append(cap)
async def send_and_wait() -> None: ts = Timestamp.now() sock_tx.send(b"".join( UDPFrame( priority=Priority.HIGH, transfer_id=0, index=0, end_of_transfer=True, payload=memoryview(str(ts).encode()), ).compile_header_and_payload())) await (asyncio.sleep(0.5)) # Let the handler run in the background.
def trn( monotonic_ns: int, transfer_id: int, fragmented_payload: typing.Sequence[typing.Union[bytes, str, memoryview]] ) -> TransferFrom: return TransferFrom( timestamp=Timestamp(system_ns=0, monotonic_ns=monotonic_ns), priority=priority, transfer_id=transfer_id, fragmented_payload=[ memoryview(x if isinstance(x, (bytes, memoryview)) else x.encode()) for x in fragmented_payload ], source_node_id=source_node_id, )
def _read_batch(self) -> typing.List[typing.Tuple[Timestamp, Envelope]]: batch: typing.List[typing.Tuple[Timestamp, Envelope]] = [] while not self._closed: msg = self._bus.recv(0.0 if batch else self._MAXIMAL_TIMEOUT_SEC) if msg is None: break timestamp = Timestamp.now() # TODO: use accurate timestamping loopback = False # TODO: no possibility to get real loopback yet frame = self._parse_native_frame(msg) if frame is not None: batch.append((timestamp, Envelope(frame, loopback))) return batch
def _unittest_frame_compile_message() -> None: from pyuavcan.transport import Priority, MessageDataSpecifier, Timestamp f = SerialFrame(timestamp=Timestamp.now(), priority=Priority.HIGH, source_node_id=SerialFrame.FRAME_DELIMITER_BYTE, destination_node_id=SerialFrame.ESCAPE_PREFIX_BYTE, data_specifier=MessageDataSpecifier(12345), data_type_hash=0xdead_beef_bad_c0ffe, transfer_id=1234567890123456789, index=1234567, end_of_transfer=True, payload=memoryview(b'abcd\x9Eef\x8E')) buffer = bytearray(0 for _ in range(1000)) mv = f.compile_into(buffer) assert mv[0] == SerialFrame.FRAME_DELIMITER_BYTE assert mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE segment = bytes(mv[1:-1]) assert SerialFrame.FRAME_DELIMITER_BYTE not in segment # Header validation assert segment[0] == _VERSION assert segment[1] == int(Priority.HIGH) assert segment[2] == SerialFrame.ESCAPE_PREFIX_BYTE assert (segment[3], segment[4]) == (SerialFrame.FRAME_DELIMITER_BYTE ^ 0xFF, 0) assert segment[5] == SerialFrame.ESCAPE_PREFIX_BYTE assert (segment[6], segment[7]) == (SerialFrame.ESCAPE_PREFIX_BYTE ^ 0xFF, 0) assert segment[8:10] == 12345 .to_bytes(2, 'little') assert segment[10:18] == 0xdead_beef_bad_c0ffe .to_bytes(8, 'little') assert segment[18:26] == 1234567890123456789 .to_bytes(8, 'little') assert segment[26:30] == (1234567 + 0x8000_0000).to_bytes(4, 'little') assert segment[30:34] == b'\x00' * 4 # Payload validation assert segment[34:38] == b'abcd' assert segment[38] == SerialFrame.ESCAPE_PREFIX_BYTE assert segment[39] == 0x9E ^ 0xFF assert segment[40:42] == b'ef' assert segment[42] == SerialFrame.ESCAPE_PREFIX_BYTE assert segment[43] == 0x8E ^ 0xFF # CRC validation header = SerialFrame.HEADER_STRUCT.pack(_VERSION, int(f.priority), f.source_node_id, f.destination_node_id, 12345, f.data_type_hash, f.transfer_id, f.index + 0x8000_0000) assert segment[44:] == pyuavcan.transport.commons.crc.CRC32C.new(header, f.payload).value_as_bytes
def _unittest_udp_tracer() -> None: from pytest import approx from ipaddress import ip_address from pyuavcan.transport import Priority, ServiceDataSpecifier from pyuavcan.transport.udp import UDPTransport from ._ip import MACHeader, IPHeader, UDPHeader, service_data_specifier_to_udp_port tr = UDPTransport.make_tracer() ts = Timestamp.now() ds = ServiceDataSpecifier(11, ServiceDataSpecifier.Role.RESPONSE) trace = tr.update( UDPCapture( ts, RawPacket( MACHeader(memoryview(b""), memoryview(b"")), IPHeader(ip_address("127.0.0.42"), ip_address("127.0.0.63")), UDPHeader(12345, service_data_specifier_to_udp_port(ds)), memoryview(b"".join( UDPFrame( priority=Priority.SLOW, transfer_id=1234567890, index=0, end_of_transfer=True, payload=memoryview(b"Hello world!"), ).compile_header_and_payload())), ), )) 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 == 42 assert trace.transfer.metadata.session_specifier.destination_node_id == 63 assert trace.transfer.metadata.session_specifier.data_specifier == ds assert trace.transfer.fragmented_payload == [memoryview(b"Hello world!")] assert None is tr.update( pyuavcan.transport.Capture(ts)) # Another transport, ignored. assert None is tr.update( UDPCapture( # Malformed frame. ts, RawPacket( MACHeader(memoryview(b""), memoryview(b"")), IPHeader(ip_address("127.0.0.42"), ip_address("127.1.0.63")), UDPHeader(1, 1), memoryview(b""), ), ))
def _finalize(self, known_invalid: bool) -> None: if not self._buffer or (len(self._buffer) == 1 and self._buffer[0] == SerialFrame.FRAME_DELIMITER_BYTE): # Avoid noise in the OOB output during normal operation. # TODO: this is a hack in place of the proper on-the-fly COBS parser. return buf = memoryview(self._buffer) self._buffer = bytearray( ) # There are memoryview instances pointing to the old buffer! ts = self._timestamp or Timestamp.now() self._timestamp = None parsed: typing.Optional[SerialFrame] = None if (not known_invalid) and len(buf) <= self._max_frame_size_bytes: parsed = SerialFrame.parse_from_cobs_image(buf) self._callback(ts, buf, parsed)
async def _unittest_issue_120() -> None: from pyuavcan.transport import MessageDataSpecifier, PayloadMetadata, Transfer from pyuavcan.transport import Priority, Timestamp, OutputSessionSpecifier from .media.mock import MockMedia asyncio.get_running_loop().slow_callback_duration = 5.0 peers: typing.Set[MockMedia] = set() media = MockMedia(peers, 8, 10) tr = can.CANTransport(media, 42) assert tr.protocol_parameters.transfer_id_modulo == 32 feedback_collector = _FeedbackCollector() ses = tr.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), PayloadMetadata(1024)) ses.enable_feedback(feedback_collector.give) for i in range(70): ts = Timestamp.now() assert await ses.send( Transfer( timestamp=ts, priority=Priority.SLOW, transfer_id=i, fragmented_payload=[_mem(str(i))] * 7, # Ensure both single- and multiframe ), tr.loop.time() + 1.0, ) await asyncio.sleep(0.1) fb = feedback_collector.take() assert fb.original_transfer_timestamp == ts num_frames = (10 * 1) + (60 * 3) # 10 single-frame, 60 multi-frame assert 70 == ses.sample_statistics().transfers assert num_frames == ses.sample_statistics().frames assert 0 == tr.sample_statistics().in_frames # loopback not included here assert 70 == tr.sample_statistics( ).in_frames_loopback # only first frame of each transfer assert num_frames == tr.sample_statistics().out_frames assert 70 == tr.sample_statistics( ).out_frames_loopback # only first frame of each transfer assert 0 == tr.sample_statistics().lost_loopback_frames
def _unittest_can_capture() -> None: from pyuavcan.transport import MessageDataSpecifier from .media import FrameFormat from ._identifier import MessageCANID ts = Timestamp.now() payload = bytearray(b"123\x0A") cap = CANCapture( ts, DataFrame( FrameFormat.EXTENDED, MessageCANID(Priority.SLOW, 42, 3210).compile([memoryview(payload)]), payload, ), own=True, ) print(cap) parsed = cap.parse() assert parsed is not None ss, prio, uf = parsed assert ss.source_node_id == 42 assert ss.destination_node_id is None assert isinstance(ss.data_specifier, MessageDataSpecifier) assert ss.data_specifier.subject_id == 3210 assert prio == Priority.SLOW assert uf.transfer_id == 0x0A assert uf.padded_payload == b"123" assert not uf.start_of_transfer assert not uf.end_of_transfer assert not uf.toggle_bit # Invalid CAN ID assert None is CANCapture( ts, DataFrame(FrameFormat.BASE, 123, payload), own=True).parse() # Invalid CAN payload assert (None is CANCapture( ts, DataFrame(FrameFormat.EXTENDED, MessageCANID(Priority.SLOW, 42, 3210).compile([]), bytearray()), own=True, ).parse())
def _unittest_frame_compile_service() -> None: from pyuavcan.transport import Priority, ServiceDataSpecifier, Timestamp f = SerialFrame(timestamp=Timestamp.now(), priority=Priority.FAST, source_node_id=SerialFrame.FRAME_DELIMITER_BYTE, destination_node_id=None, data_specifier=ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE), data_type_hash=0xdead_beef_bad_c0ffe, transfer_id=1234567890123456789, index=1234567, end_of_transfer=False, payload=memoryview(b'')) buffer = bytearray(0 for _ in range(50)) mv = f.compile_into(buffer) assert mv[0] == mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE segment = bytes(mv[1:-1]) assert SerialFrame.FRAME_DELIMITER_BYTE not in segment # Header validation assert segment[0] == _VERSION assert segment[1] == int(Priority.FAST) assert segment[2] == SerialFrame.ESCAPE_PREFIX_BYTE assert (segment[3], segment[4]) == (SerialFrame.FRAME_DELIMITER_BYTE ^ 0xFF, 0) assert (segment[5], segment[6]) == (0xFF, 0xFF) assert segment[7:9] == ((1 << 15) | (1 << 14) | 123) .to_bytes(2, 'little') assert segment[9:17] == 0xdead_beef_bad_c0ffe .to_bytes(8, 'little') assert segment[17:25] == 1234567890123456789 .to_bytes(8, 'little') assert segment[25:29] == 1234567 .to_bytes(4, 'little') assert segment[29:33] == b'\x00' * 4 # CRC validation header = SerialFrame.HEADER_STRUCT.pack(_VERSION, int(f.priority), f.source_node_id, _ANONYMOUS_NODE_ID, (1 << 15) | (1 << 14) | 123, f.data_type_hash, f.transfer_id, f.index) assert segment[33:] == pyuavcan.transport.commons.crc.CRC32C.new(header, f.payload).value_as_bytes
def _reader_thread_func(self) -> None: in_bytes_count = 0 def callback(ts: Timestamp, buf: memoryview, frame: typing.Optional[SerialFrame]) -> None: item = buf if frame is None else frame self._loop.call_soon_threadsafe( self._handle_received_item_and_update_stats, ts, item, in_bytes_count) if self._capture_handlers: pyuavcan.util.broadcast(self._capture_handlers)(SerialCapture( ts, SerialCapture.Direction.RX, buf)) try: parser = StreamParser(callback, max(self.VALID_MTU_RANGE)) assert abs(self._serial_port.timeout - _SERIAL_PORT_READ_TIMEOUT) < 0.1 while not self._closed and self._serial_port.is_open: chunk = self._serial_port.read( max(1, self._serial_port.inWaiting())) chunk_ts = Timestamp.now() in_bytes_count += len(chunk) parser.process_next_chunk(chunk, chunk_ts) except Exception as ex: # pragma: no cover if self._closed or not self._serial_port.is_open: _logger.debug( "%s: The serial port is closed, exception ignored: %r", self, ex) else: _logger.exception( "%s: Reader thread has failed, the instance with port %s will be terminated: %s", self, self._serial_port, ex, ) self._closed = True self._serial_port.close() finally: _logger.debug("%s: Reader thread is exiting. Head aega.", self)
def _unittest_serialize_transfer() -> None: from pyuavcan.transport import Priority, Timestamp timestamp = Timestamp.now() priority = Priority.NOMINAL transfer_id = 12345678901234567890 def construct_frame(index: int, end_of_transfer: bool, payload: memoryview) -> Frame: return Frame(timestamp=timestamp, priority=priority, transfer_id=transfer_id, index=index, end_of_transfer=end_of_transfer, payload=payload) assert [ construct_frame(0, True, memoryview(b'hello world')), ] == list( serialize_transfer( [memoryview(b'hello'), memoryview(b' '), memoryview(b'world')], 100, construct_frame)) assert [ construct_frame(0, True, memoryview(b'')), ] == list(serialize_transfer([], 100, construct_frame)) hello_world_crc = pyuavcan.transport.commons.crc.CRC32C() hello_world_crc.add(b'hello world') assert [ construct_frame(0, False, memoryview(b'hello')), construct_frame(1, False, memoryview(b' worl')), construct_frame(2, True, memoryview(b'd' + hello_world_crc.value_as_bytes)), ] == list( serialize_transfer( [memoryview(b'hello'), memoryview(b' '), memoryview(b'world')], 5, construct_frame))
def _read_frame(self, ts_mono_ns: int) -> typing.Tuple[Timestamp, Envelope]: while True: data, ancdata, msg_flags, _addr = self._sock.recvmsg( self._native_frame_size, self._ancillary_data_buffer_size ) assert msg_flags & socket.MSG_TRUNC == 0, "The data buffer is not large enough" assert msg_flags & socket.MSG_CTRUNC == 0, "The ancillary data buffer is not large enough" loopback = bool(msg_flags & socket.MSG_CONFIRM) ts_system_ns = 0 for cmsg_level, cmsg_type, cmsg_data in ancdata: if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMP: sec, usec = _TIMEVAL_STRUCT.unpack(cmsg_data) ts_system_ns = (sec * 1_000_000 + usec) * 1000 else: assert False, f"Unexpected ancillary data: {cmsg_level}, {cmsg_type}, {cmsg_data!r}" assert ts_system_ns > 0, "Missing the timestamp; does the driver support timestamping?" timestamp = Timestamp(system_ns=ts_system_ns, monotonic_ns=ts_mono_ns) out = SocketCANMedia._parse_native_frame(data) if out is not None: return timestamp, Envelope(out, loopback=loopback)
def _unittest_frame_compile_message() -> None: from pyuavcan.transport import Priority, MessageDataSpecifier, Timestamp f = SerialFrame(timestamp=Timestamp.now(), priority=Priority.HIGH, source_node_id=SerialFrame.FRAME_DELIMITER_BYTE, destination_node_id=SerialFrame.FRAME_DELIMITER_BYTE, data_specifier=MessageDataSpecifier(12345), data_type_hash=0xdead_beef_bad_c0ffe, transfer_id=1234567890123456789, index=1234567, end_of_transfer=True, payload=memoryview(b'abcd\x00ef\x00')) buffer = bytearray(0 for _ in range(1000)) mv = f.compile_into(buffer) assert mv[0] == SerialFrame.FRAME_DELIMITER_BYTE assert mv[-1] == SerialFrame.FRAME_DELIMITER_BYTE segment_cobs = bytes(mv[1:-1]) assert SerialFrame.FRAME_DELIMITER_BYTE not in segment_cobs segment = cobs.decode(segment_cobs) # Header validation assert segment[0] == _VERSION assert segment[1] == int(Priority.HIGH) assert (segment[2], segment[3]) == (SerialFrame.FRAME_DELIMITER_BYTE, 0) assert (segment[4], segment[5]) == (SerialFrame.FRAME_DELIMITER_BYTE, 0) assert segment[6:8] == 12345 .to_bytes(2, 'little') assert segment[8:16] == 0xdead_beef_bad_c0ffe .to_bytes(8, 'little') assert segment[16:24] == 1234567890123456789 .to_bytes(8, 'little') assert segment[24:28] == (1234567 + 0x8000_0000).to_bytes(4, 'little') # Header CRC here # Payload validation assert segment[32:40] == b'abcd\x00ef\x00' assert segment[40:] == pyuavcan.transport.commons.crc.CRC32C.new(f.payload).value_as_bytes