Skip to content

Commit 13c0e75

Browse files
authored
Merge pull request hardbyte#1015 from hardbyte/socketcan-timestamp-ns
Add nanosecond resolution time stamping to socketcan
2 parents 0b7e615 + 5794fd0 commit 13c0e75

File tree

3 files changed

+46
-17
lines changed

3 files changed

+46
-17
lines changed

can/interfaces/socketcan/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
Defines shared CAN constants.
33
"""
44

5+
# Generic socket constants
6+
SO_TIMESTAMPNS = 35
7+
58
CAN_ERR_FLAG = 0x20000000
69
CAN_RTR_FLAG = 0x40000000
710
CAN_EFF_FLAG = 0x80000000

can/interfaces/socketcan/socketcan.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
log.error("fcntl not available on this platform")
2828

2929

30+
try:
31+
from socket import CMSG_SPACE
32+
33+
CMSG_SPACE_available = True
34+
except ImportError:
35+
CMSG_SPACE_available = False
36+
log.error("socket.CMSG_SPACE not available on this platform")
37+
38+
3039
import can
3140
from can import Message, BusABC
3241
from can.broadcastmanager import (
@@ -38,6 +47,7 @@
3847
from can.interfaces.socketcan.constants import * # CAN_RAW, CAN_*_FLAG
3948
from can.interfaces.socketcan.utils import pack_filters, find_available_interfaces
4049

50+
4151
# Setup BCM struct
4252
def bcm_header_factory(
4353
fields: List[Tuple[str, Union[Type[ctypes.c_uint32], Type[ctypes.c_long]]]],
@@ -494,6 +504,8 @@ def bind_socket(sock: socket.socket, channel: str = "can0") -> None:
494504
495505
:param sock:
496506
The socket to be bound
507+
:param channel:
508+
The channel / interface to bind to
497509
:raises OSError:
498510
If the specified interface isn't found.
499511
"""
@@ -517,24 +529,27 @@ def capture_message(
517529
"""
518530
# Fetching the Arb ID, DLC and Data
519531
try:
532+
cf, ancillary_data, msg_flags, addr = sock.recvmsg(
533+
CANFD_MTU, RECEIVED_ANCILLARY_BUFFER_SIZE
534+
)
520535
if get_channel:
521-
cf, _, msg_flags, addr = sock.recvmsg(CANFD_MTU)
522536
channel = addr[0] if isinstance(addr, tuple) else addr
523537
else:
524-
cf, _, msg_flags, _ = sock.recvmsg(CANFD_MTU)
525538
channel = None
526-
except socket.error as exc:
527-
raise can.CanError("Error receiving: %s" % exc)
539+
except socket.error as error:
540+
raise can.CanError(f"Error receiving: {error}")
528541

529542
can_id, can_dlc, flags, data = dissect_can_frame(cf)
530-
# log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data)
531543

532544
# Fetching the timestamp
533-
binary_structure = "@LL"
534-
res = fcntl.ioctl(sock.fileno(), SIOCGSTAMP, struct.pack(binary_structure, 0, 0))
535-
536-
seconds, microseconds = struct.unpack(binary_structure, res)
537-
timestamp = seconds + microseconds * 1e-6
545+
assert len(ancillary_data) == 1, "only requested a single extra field"
546+
cmsg_level, cmsg_type, cmsg_data = ancillary_data[0]
547+
assert (
548+
cmsg_level == socket.SOL_SOCKET and cmsg_type == SO_TIMESTAMPNS
549+
), "received control message type that was not requested"
550+
# see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
551+
seconds, nanoseconds = RECEIVED_TIMESTAMP_STRUCT.unpack_from(cmsg_data)
552+
timestamp = seconds + nanoseconds * 1e-9
538553

539554
# EXT, RTR, ERR flags -> boolean attributes
540555
# /* special address description flags for the CAN_ID */
@@ -574,11 +589,15 @@ def capture_message(
574589
data=data,
575590
)
576591

577-
# log_rx.debug('Received: %s', msg)
578-
579592
return msg
580593

581594

595+
# Constants needed for precise handling of timestamps
596+
if CMSG_SPACE_available:
597+
RECEIVED_TIMESTAMP_STRUCT = struct.Struct("@II")
598+
RECEIVED_ANCILLARY_BUFFER_SIZE = CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size)
599+
600+
582601
class SocketcanBus(BusABC):
583602
"""A SocketCAN interface to CAN.
584603
@@ -647,7 +666,7 @@ def __init__(
647666
except socket.error as error:
648667
log.error("Could not receive own messages (%s)", error)
649668

650-
# enable CAN-FD frames
669+
# enable CAN-FD frames if desired
651670
if fd:
652671
try:
653672
self.socket.setsockopt(SOL_CAN_RAW, CAN_RAW_FD_FRAMES, 1)
@@ -660,6 +679,13 @@ def __init__(
660679
except socket.error as error:
661680
log.error("Could not enable error frames (%s)", error)
662681

682+
# enable nanosecond resolution timestamping
683+
# we can always do this since
684+
# 1) is is guaranteed to be at least as precise as without
685+
# 2) it is available since Linux 2.6.22, and CAN support was only added afterward
686+
# so this is always supported by the kernel
687+
self.socket.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1)
688+
663689
bind_socket(self.socket, channel)
664690
kwargs.update(
665691
{

doc/interfaces/socketcan.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ The `SocketCAN`_ documentation can be found in the Linux kernel docs at
1818
.. important::
1919

2020
`python-can` versions before 2.2 had two different implementations named
21-
``socketcan_ctypes`` and ``socketcan_native``. These are now
22-
deprecated and the aliases to ``socketcan`` will be removed in
23-
version 4.0. 3.x releases raise a DeprecationWarning.
21+
``socketcan_ctypes`` and ``socketcan_native``. These were removed in
22+
version 4.0.0 after a deprecation period.
2423

2524

2625
Socketcan Quickstart
@@ -248,7 +247,8 @@ The :class:`~can.interfaces.socketcan.SocketcanBus` specializes :class:`~can.Bus
248247
to ensure usage of SocketCAN Linux API. The most important differences are:
249248

250249
- usage of SocketCAN BCM for periodic messages scheduling;
251-
- filtering of CAN messages on Linux kernel level.
250+
- filtering of CAN messages on Linux kernel level;
251+
- usage of nanosecond timings from the kernel.
252252

253253
.. autoclass:: can.interfaces.socketcan.SocketcanBus
254254
:members:

0 commit comments

Comments
 (0)