Skip to content

Wireshark Integration (PCAP-NG)

CAN & CANopen Studio writes captures in PCAP-NG with the LINKTYPE_CAN_SOCKETCAN encapsulation, which Wireshark reads natively — the same format a libpcap capture of a Linux SocketCAN interface produces. No plugin, no conversion step.

Wireshark then brings its own dissectors: CANopen, SAE J1939, ISO 15765 (ISO-TP), DeviceNet, AUTOSAR NM, XCP. The studio ships none of its own; it only hands over the frames in a shape Wireshark recognises.


Writing a capture file

# Everything the adapter sees, into a file
uv run can-sniffer -I slcan -b 500000 --pcap bus.pcapng

# Bounded capture: 10 seconds of a virtual bus
uv run can-sniffer -I virtual --simulate -t 10 --pcap sim.pcapng

Open bus.pcapng in Wireshark, or check it from the shell:

& "C:\Program Files\Wireshark\capinfos.exe" bus.pcapng
& "C:\Program Files\Wireshark\tshark.exe" -r bus.pcapng
File encapsulation:  SocketCAN
File timestamp precision:  microseconds (6)

1   0.000000    CAN 16 ID: 128 (0x080), Length: 0
2   0.001000    CAN 16 ID: 1793 (0x701), Length: 1
3   0.002000    CAN 16 ID: 385 (0x181), Length: 4

The capture is never filtered

--pcap records every frame the adapter delivers, before the display filters (--id, --extended-only, …) are applied. Wireshark's own filters are better than ours, and a capture that silently omits frames is a trap for whoever reads it later.


Live capture through a named pipe

Wireshark can read a capture as it is being written. Wireshark connects to a pipe; it never creates one — so the studio is the server, and it goes first.

Windows

# 1. The studio first: it creates the pipe, then waits for a reader
uv run can-sniffer -I slcan -b 500000 --pcap \\.\pipe\canopen-studio

# 2. Then Wireshark, in another terminal, connecting to that pipe
& "C:\Program Files\Wireshark\Wireshark.exe" -i \\.\pipe\canopen-studio -k

The sniffer prints Waiting for Wireshark to connect … and blocks there. That is not a hang: nothing can be captured until something is listening.

Linux / macOS

Here the FIFO is made up front and the rendezvous happens on open, so either order works:

mkfifo /tmp/canopen-studio
wireshark -i /tmp/canopen-studio -k &
uv run can-sniffer -I socketcan --channel can0 --pcap /tmp/canopen-studio

If Wireshark is closed mid-session the sniffer reports PCAP-NG capture stopped and keeps sniffing — a viewer going away is not a reason to lose the session.


Decoding frames as CANopen

Wireshark dissects the CAN layer on its own, but the payload above it is ambiguous: the same 11-bit ID space is CANopen on one bus and something proprietary on the next. So the CANopen dissector has to be selected.

In the GUI: Analyze → Decode As…, add an entry for the CAN next level dissector field, choose CANopen.

From the shell:

& "C:\Program Files\Wireshark\tshark.exe" -r bus.pcapng -d can.subdissector=canopen -V
CANopen
    COB-ID: 0x00000601
        [Function code: 0xc]
        [Node-ID: 0x01]
    Type: Default-SDO (rx)
        SDO command byte: 0x40, Client command specifier: Initiate upload request
            010. .... = Client command specifier: Initiate upload request (2)
        OD main-index: Device type (0x1000)
        OD sub-index: 0x00

Use j1939 instead for 29-bit commercial-vehicle traffic, or iso15765 for OBD-II and UDS diagnostics carried over ISO-TP.


Useful display filters

Filter Shows
can.id == 0x080 SYNC objects
can.id >= 0x701 && can.id <= 0x77f heartbeats
can.id >= 0x581 && can.id <= 0x5ff SDO responses
can.flags.rtr == 1 remote requests
can.flags.err == 1 error frames
canopen.sdo.abortcode SDO aborts, once decoded as CANopen

Capturing from Wireshark itself (extcap)

The pipe above works, but it puts the studio first and Wireshark second. An extcap turns that around: canopen-extcap, added in 0.7.0, puts the studio's buses in Wireshark's own interface list, where you start a capture by double-clicking one.

This matters most away from Linux. Wireshark can only capture CAN through SocketCAN, a kernel feature, so on macOS and Windows its list has nothing CAN to offer at all.

Build it and drop it in:

cargo build --release -p canopen-cli --bin canopen-extcap

Wireshark's own Help → About Wireshark → Folders names the directory to copy the binary into — the Personal Extcap path, usually:

Platform Personal extcap directory
Windows %APPDATA%\Wireshark\extcap\
macOS, Linux ~/.config/wireshark/extcap/

Restart Wireshark, or refresh the interface list, and two entries appear:

Interface What it captures
CANopen Studio: CAN over UDP (network bridge) The UDP transport — a studio bridging a real bus, or another machine on the group. Its gear icon sets the listen port, peer address and peer port (1750, 127.0.0.1, 1750 by default)
CANopen Studio: virtual simulator (demo bus) The bundled simulator on a loopback pair, for a bus with no hardware and nothing else running

Only those two are advertised, because they are the two canopen-core can really open. The serial and SocketCAN backends live in the Python studio and have no Rust equivalent yet; listing them here would put entries in Wireshark's list that cannot be captured from.

The frames arrive with the same LINKTYPE_CAN_SOCKETCAN encapsulation as a capture file, so Decode As and the display filters apply unchanged.


From your own code

The writer lives in the Rust core and is exposed to Python, so any script can produce a capture:

from canopen_studio import canopen_core

with canopen_core.PcapNgWriter("bus.pcapng", "slcan COM4") as capture:
    capture.write_frame(0x080, b"")  # SYNC
    capture.write_frame(0x701, bytes([0x05]))  # heartbeat
    capture.write_frame(0x18EAFFFE, bytes([0xEE]), None, True)  # 29-bit
    capture.write_frame(0x123, b"", None, False, True, False, 8)  # RTR, DLC 8

write_frame(id, data, timestamp_us, is_extended, is_remote, is_error, dlc) — everything after id is optional. The timestamp is microseconds since the Unix epoch; left out, the current time is used. dlc only matters for a remote frame, which requests a length it does not carry.

In Rust:

use canopen_core::{CanFrame, PcapNgWriter};

let mut capture = PcapNgWriter::new(File::create("bus.pcapng")?, "can0")?;
capture.write_frame(&CanFrame::new(0x181, &[0x10, 0x27])?)?;

Every frame is flushed as it is written, which is what makes the pipe work.