Skip to content

Commit 00fc24f

Browse files
committed
Add PcapngWriter
1 parent 60a2d66 commit 00fc24f

File tree

6 files changed

+134
-6
lines changed

6 files changed

+134
-6
lines changed

can/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"MessageSync",
4545
"ModifiableCyclicTaskABC",
4646
"Notifier",
47+
"PcapngWriter",
4748
"Printer",
4849
"RedirectReader",
4950
"RestartableCyclicTaskABC",
@@ -109,6 +110,7 @@
109110
Logger,
110111
LogReader,
111112
MessageSync,
113+
PcapngWriter,
112114
MF4Reader,
113115
MF4Writer,
114116
Printer,

can/io/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"MF4Reader",
2121
"MF4Writer",
2222
"MessageSync",
23+
"PcapngWriter",
2324
"Printer",
2425
"SizedRotatingLogger",
2526
"SqliteReader",
@@ -52,6 +53,7 @@
5253
from .canutils import CanutilsLogReader, CanutilsLogWriter
5354
from .csv import CSVReader, CSVWriter
5455
from .mf4 import MF4Reader, MF4Writer
56+
from .pcapng import PcapngWriter
5557
from .printer import Printer
5658
from .sqlite import SqliteReader, SqliteWriter
5759
from .trc import TRCFileVersion, TRCReader, TRCWriter

can/io/logger.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from .printer import Printer
4141
from .sqlite import SqliteWriter
4242
from .trc import TRCWriter
43+
from .pcapng import PcapngWriter
4344

4445
#: A map of file suffixes to their corresponding
4546
#: :class:`can.io.generic.MessageWriter` class
@@ -50,6 +51,7 @@
5051
".db": SqliteWriter,
5152
".log": CanutilsLogWriter,
5253
".mf4": MF4Writer,
54+
".pcapng": PcapngWriter,
5355
".trc": TRCWriter,
5456
".txt": Printer,
5557
}

can/io/pcapng.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
Contains handling of pcapng logging files.
3+
4+
pcapng file is a binary file format used for packet capture files.
5+
Spec: https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html
6+
"""
7+
8+
import logging
9+
10+
from typing import Any, BinaryIO, Dict, Optional, Union
11+
12+
from ..message import Message
13+
from ..typechecking import StringPathLike, Channel
14+
from .generic import BinaryIOMessageWriter
15+
from ..socketcan_common import build_can_frame, CAN_FRAME_HEADER_STRUCT_BE
16+
17+
logger = logging.getLogger("can.io.pcapng")
18+
19+
try:
20+
import pcapng
21+
from pcapng import blocks
22+
except ImportError:
23+
pcapng = None
24+
25+
26+
# https://www.tcpdump.org/linktypes.html
27+
# https://www.tcpdump.org/linktypes/LINKTYPE_CAN_SOCKETCAN.html
28+
LINKTYPE_CAN_SOCKETCAN = 227
29+
30+
31+
class PcapngWriter(BinaryIOMessageWriter):
32+
"""
33+
Logs CAN data to an pcapng file supported by Wireshark and other tools.
34+
"""
35+
36+
def __init__(
37+
self,
38+
file: Union[StringPathLike, BinaryIO],
39+
append: bool = False,
40+
tsresol: int = 9,
41+
**kwargs: Any,
42+
) -> None:
43+
"""
44+
:param file:
45+
A path-like object or as file-like object to write to.
46+
If this is a file-like object, is has to be opened in
47+
binary write mode, not text write mode.
48+
49+
:param append:
50+
If True, the file will be opened in append mode. Otherwise,
51+
it will be opened in write mode. The default is False.
52+
53+
:param tsresol:
54+
The time resolution of the timestamps in the pcapng file,
55+
expressed as -log10(unit in seconds),
56+
e.g. 9 for nanoseconds, 6 for microseconds.
57+
The default is 9, which corresponds to nanoseconds.
58+
.
59+
"""
60+
if pcapng is None:
61+
raise NotImplementedError(
62+
"The python-pcapng package was not found. Install python-can with "
63+
"the optional dependency [pcapng] to use the PcapngWriter."
64+
)
65+
66+
mode = "wb+"
67+
if append:
68+
mode = "ab+"
69+
70+
# pcapng supports concatenation, and thus append
71+
super().__init__(file, mode=mode)
72+
self._header_block = blocks.SectionHeader(endianness=">")
73+
self._writer = pcapng.FileWriter(self.file, self._header_block)
74+
self._idbs: Dict[Channel, blocks.InterfaceDescription] = {}
75+
self.tsresol = tsresol
76+
77+
def _resolve_idb(self, channel: Optional[Channel]) -> Any:
78+
channel_name = str(channel)
79+
if channel is None:
80+
channel_name = "can0"
81+
82+
if channel_name not in self._idbs:
83+
idb = blocks.InterfaceDescription(
84+
section=self._header_block.section,
85+
link_type=LINKTYPE_CAN_SOCKETCAN,
86+
options={
87+
"if_name": channel_name,
88+
"if_tsresol": bytes([self.tsresol]), # nanoseconds
89+
},
90+
endianness=">", # big
91+
)
92+
self._header_block.register_interface(idb)
93+
self._writer.write_block(idb)
94+
self._idbs[channel_name] = idb
95+
96+
return self._idbs[channel_name]
97+
98+
def on_message_received(self, msg: Message) -> None:
99+
idb: blocks.InterfaceDescription = self._resolve_idb(msg.channel)
100+
timestamp_units = int(msg.timestamp * 10**self.tsresol)
101+
self._writer.write_block(
102+
blocks.EnhancedPacket(
103+
self._header_block.section,
104+
interface_id=idb.interface_id,
105+
packet_data=build_can_frame(msg, struct=CAN_FRAME_HEADER_STRUCT_BE),
106+
# timestamp (in tsresol units) = timestamp_high << 32 + timestamp_low
107+
timestamp_high=timestamp_units >> 32,
108+
timestamp_low=timestamp_units & 0xFFFFFFFF,
109+
endianness=">", # big
110+
)
111+
)

can/socketcan_common.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,25 @@
8787
# The 32bit can id is directly followed by the 8bit data link count
8888
# The data field is aligned on an 8 byte boundary, hence we add padding
8989
# which aligns the data field to an 8 byte boundary.
90+
91+
# host-endian for communication with kernel
9092
CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB1xB")
93+
# big-endian for pcapng
94+
CAN_FRAME_HEADER_STRUCT_BE = struct.Struct(">IBB1xB")
9195

9296

9397
log = logging.getLogger(__name__)
9498

9599

96-
def parse_can_frame(cf: bytes) -> Message:
100+
def parse_can_frame(
101+
cf: bytes, struct: struct.Struct = CAN_FRAME_HEADER_STRUCT
102+
) -> Message:
97103
"""Parse a CAN frame.
98104
99105
:param cf: A CAN frame in socketcan format
100106
:return: A :class:`~can.Message` object with the parsed data
101107
"""
102-
can_id, can_dlc, flags, data = dissect_can_frame(cf)
108+
can_id, can_dlc, flags, data = dissect_can_frame(cf, struct)
103109

104110
# EXT, RTR, ERR flags -> boolean attributes
105111
# /* special address description flags for the CAN_ID */
@@ -148,7 +154,9 @@ def _compose_arbitration_id(message: Message) -> int:
148154
return can_id
149155

150156

151-
def build_can_frame(msg: Message) -> bytes:
157+
def build_can_frame(
158+
msg: Message, struct: struct.Struct = CAN_FRAME_HEADER_STRUCT
159+
) -> bytes:
152160
"""CAN frame packing (see 'struct can_frame' in <linux/can.h>)
153161
154162
:param msg: A :class:`~can.Message` object to convert to a CAN frame
@@ -178,7 +186,7 @@ def build_can_frame(msg: Message) -> bytes:
178186
data_len = msg.dlc
179187
else:
180188
data_len = min(i for i in can.util.CAN_FD_DLC if i >= len(msg.data))
181-
header = CAN_FRAME_HEADER_STRUCT.pack(can_id, data_len, flags, msg.dlc)
189+
header = struct.pack(can_id, data_len, flags, msg.dlc)
182190
return header + data
183191

184192

@@ -188,14 +196,16 @@ def is_frame_fd(frame: bytes) -> bool:
188196
return len(frame) == CANFD_MTU
189197

190198

191-
def dissect_can_frame(frame: bytes) -> Tuple[int, int, int, bytes]:
199+
def dissect_can_frame(
200+
frame: bytes, struct: struct.Struct = CAN_FRAME_HEADER_STRUCT
201+
) -> Tuple[int, int, int, bytes]:
192202
"""Dissect a CAN frame into its components.
193203
194204
:param frame: A CAN frame in socketcan format
195205
:return: Tuple of (CAN ID, CAN DLC, flags, data)
196206
"""
197207

198-
can_id, data_len, flags, len8_dlc = CAN_FRAME_HEADER_STRUCT.unpack_from(frame)
208+
can_id, data_len, flags, len8_dlc = struct.unpack_from(frame)
199209

200210
if data_len not in can.util.CAN_FD_DLC:
201211
data_len = min(i for i in can.util.CAN_FD_DLC if i >= data_len)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ viewer = [
8383
"windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'"
8484
]
8585
mf4 = ["asammdf>=6.0.0"]
86+
pcapng = ["python-pcapng>=2.1.1"]
8687

8788
[tool.setuptools.dynamic]
8889
readme = { file = "README.rst" }

0 commit comments

Comments
 (0)