diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index c1f62b69..c4d9c22e 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -10,7 +10,8 @@ jobs: pycyphal-test: name: Test PyCyphal # Run on push OR on 3rd-party PR. - if: (github.event_name == 'push') || (github.event.pull_request.author_association == 'NONE') + # https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=edited#pull_request + if: (github.event_name == 'push') || github.event.pull_request.head.repo.fork strategy: fail-fast: false matrix: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4c3f1119..0d5f723b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,9 @@ v1.19 ----- - Implement configure_acceptance_filters for socketcan. +- **v1.19.1:** + Fix socketcan timestamp on newer 32 bit kernel. + v1.18 ----- - Add FileClient2 which reports errors by raising exceptions. diff --git a/pycyphal/_version.py b/pycyphal/_version.py index d84d79d4..90fa0968 100644 --- a/pycyphal/_version.py +++ b/pycyphal/_version.py @@ -1 +1 @@ -__version__ = "1.19.0" +__version__ = "1.19.1" diff --git a/pycyphal/transport/can/media/socketcan/_socketcan.py b/pycyphal/transport/can/media/socketcan/_socketcan.py index ece360b8..f09d4528 100644 --- a/pycyphal/transport/can/media/socketcan/_socketcan.py +++ b/pycyphal/transport/can/media/socketcan/_socketcan.py @@ -87,7 +87,8 @@ def __init__(self, iface_name: str, mtu: int, loop: typing.Optional[asyncio.Abst self._maybe_thread: typing.Optional[threading.Thread] = None self._loopback_enabled = False - self._ancillary_data_buffer_size = socket.CMSG_SPACE(_TIMEVAL_STRUCT.size) # type: ignore + # We could receive both old and new timestamps, so we need to allocate space for both. + self._ancillary_data_buffer_size = socket.CMSG_SPACE(_TIMEVAL_STRUCT_OLD.size) + socket.CMSG_SPACE(_TIMEVAL_STRUCT_NEW.size) # type: ignore super().__init__() @@ -232,9 +233,15 @@ def _read_frame(self, ts_mono_ns: int) -> typing.Tuple[Timestamp, Envelope]: loopback = bool(msg_flags & socket.MSG_CONFIRM) # type: ignore 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) + if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMP_OLD: + # This structure provides time in platform native size + sec, usec = _TIMEVAL_STRUCT_OLD.unpack(cmsg_data) ts_system_ns = (sec * 1_000_000 + usec) * 1000 + elif cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMP_NEW: + # This structure is present only when there is a 64 bit time on a 32 bit platform + sec, usec = _TIMEVAL_STRUCT_NEW.unpack(cmsg_data) + ts_system_ns = (sec * 1_000_000 + usec) * 1000 + break # The new timestamp is preferred else: assert False, f"Unexpected ancillary data: {cmsg_level}, {cmsg_type}, {cmsg_data!r}" @@ -309,10 +316,14 @@ class _NativeFrameDataCapacity(enum.IntEnum): # __u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8))); # }; _FRAME_HEADER_STRUCT = struct.Struct("=IBB2x") # Using standard size because the native definition relies on stdint.h -_TIMEVAL_STRUCT = struct.Struct("@Ll") # Using native size because the native definition uses plain integers -# From the Linux kernel; not exposed via the Python's socket module -_SO_TIMESTAMP = 29 +# structs __kernel_old_timeval and __kernel_sock_timeval in include/uapi/linux/time_types.h +_TIMEVAL_STRUCT_OLD = struct.Struct("@ll") # Using native size because the native definition uses plain integers +_TIMEVAL_STRUCT_NEW = struct.Struct("@qq") # New structure uses s64 for seconds and microseconds + +# From the Linux kernel (include/uapi/asm-generic/socket.h); not exposed via the Python's socket module +_SO_TIMESTAMP_OLD = 29 +_SO_TIMESTAMP_NEW = 63 _SO_SNDBUF = 7 _CANFD_BRS = 1 @@ -342,7 +353,7 @@ def _make_socket(iface_name: str, can_fd: bool, native_frame_size: int) -> socke s = socket.socket(socket.PF_CAN, socket.SOCK_RAW, socket.CAN_RAW) # type: ignore try: s.bind((iface_name,)) - s.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMP, 1) # timestamping + s.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMP_OLD, 1) # timestamping default_sndbuf_size = s.getsockopt(socket.SOL_SOCKET, _SO_SNDBUF) blocking_sndbuf_size = (native_frame_size + _SKB_OVERHEAD) * _get_tx_queue_len(iface_name)