From 4df7d143a2208d887ef88a5cadd761ac6c90f2a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Jakub=C3=ADk?=
 <117373330+tomasjakubik@users.noreply.github.com>
Date: Thu, 13 Jun 2024 13:56:02 +0200
Subject: [PATCH] Implement configure_acceptance_filters for socketcan (#340)

Co-authored-by: Pavel Kirienko <pavel.kirienko@gmail.com>
---
 CHANGELOG.rst                                 |  4 +++
 pycyphal/_version.py                          |  2 +-
 .../can/media/socketcan/_socketcan.py         | 36 +++++++++++++++----
 3 files changed, 35 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index bf9938fb..4c3f1119 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,10 @@
 Changelog
 =========
 
+v1.19
+-----
+- Implement configure_acceptance_filters for socketcan.
+
 v1.18
 -----
 - Add FileClient2 which reports errors by raising exceptions.
diff --git a/pycyphal/_version.py b/pycyphal/_version.py
index 6cea18d8..d84d79d4 100644
--- a/pycyphal/_version.py
+++ b/pycyphal/_version.py
@@ -1 +1 @@
-__version__ = "1.18.0"
+__version__ = "1.19.0"
diff --git a/pycyphal/transport/can/media/socketcan/_socketcan.py b/pycyphal/transport/can/media/socketcan/_socketcan.py
index 17d68562..ece360b8 100644
--- a/pycyphal/transport/can/media/socketcan/_socketcan.py
+++ b/pycyphal/transport/can/media/socketcan/_socketcan.py
@@ -123,12 +123,11 @@ def start(self, handler: Media.ReceivedFramesHandler, no_automatic_retransmissio
     def configure_acceptance_filters(self, configuration: typing.Sequence[FilterConfiguration]) -> None:
         if self._closed:
             raise pycyphal.transport.ResourceClosedError(repr(self))
-        _logger.info(
-            "%s FIXME: acceptance filter configuration is not yet implemented; please submit patches! "
-            "Requested configuration: %s",
-            self,
-            ", ".join(map(str, configuration)),
-        )
+
+        try:
+            self._sock.setsockopt(socket.SOL_CAN_RAW, socket.CAN_RAW_FILTER, _pack_filters(configuration))  # type: ignore
+        except OSError as error:
+            _logger.error("Setting CAN filters failed: %s", error)
 
     async def send(self, frames: typing.Iterable[Envelope], monotonic_deadline: float) -> int:
         num_sent = 0
@@ -366,3 +365,28 @@ def _make_socket(iface_name: str, can_fd: bool, native_frame_size: int) -> socke
         raise
 
     return s
+
+
+def _pack_filters(configuration: typing.Sequence[FilterConfiguration]) -> bytes:
+    """Convert a list of filters into a packed structure suitable for setsockopt().
+    Inspired by python-can sources.
+    :param configuration: list of CAN filters
+    :type configuration: typing.Sequence[FilterConfiguration]
+    :return: packed structure suitable for setsockopt()
+    :rtype: bytes
+    """
+
+    can_filter_fmt = f"={2 * len(configuration)}I"
+    filter_data = []
+    for can_filter in configuration:
+        can_id = can_filter.identifier
+        can_mask = can_filter.mask
+        if can_filter.format is not None:
+            # Match on either 11-bit OR 29-bit messages instead of both
+            can_mask |= _CAN_EFF_FLAG  # Not using socket.CAN_EFF_FLAG because it is negative on 32 bit platforms
+            if can_filter.format == FrameFormat.EXTENDED:
+                can_id |= _CAN_EFF_FLAG
+        filter_data.append(can_id)
+        filter_data.append(can_mask)
+
+    return struct.pack(can_filter_fmt, *filter_data)