saleae_usb_pcap.py
#!/usr/bin/env python3
#
# saleae_usb_pcap -- convert CSV export of Saleae Logic USB analyzer to PcapNG for Wireshark
#
# author: Trevor Bentley <trevor@trevorbentley.com>
# date: 2025-10-22
# license: 0BSD
#
# Only works with USB 2.0 Full Speed devices.
#
#
# Exporting CSV from Saleae:
#
# 1) capture signal on USB D+ and D- lines
# 2) enable "USB LS and FS" analyzer in Full Speed mode with "Bytes" decode level
# 3) click "..."->"Export Table" in the Data Table section
# 4) choose All columns, All data, CSV format, and enable "Use ISO8601 timestamps"
#
#
# Process with:
#
# $ ./saleae_usb_pcap.py exported_from_saleae.csv converted.pcap
#
#
# Open with:
#
# $ wireshark converted.pcap
#
= 0x06
= 252
= 294
= 12
= 9
= 6
# PcapNG format and header examples looted from:
#
# * https://pcapng.com/
# * https://github.com/ataradov/usb-sniffer/blob/main/software/capture.c
# * https://www.tcpdump.org/linktypes.html
# * https://github.com/greatscottgadgets/packetry/blob/main/src/file.rs
#
=
return + b *
= + +
return +
=
return
=
# this is inexplicably the only packet that has to be big-endian
= +
return + +
=
+=
+=
=
=
+=
+=
+=
+=
=
=
+=
+=
+=
+=
=
=
= +
=
+=
=
=
=
=
=
=
"""The known types a UsbEvent can have"""
# States without byte representation on bus
= 0x00
= 0x01
= 0x02
= 0x03
# USB PIDs
= 0x3c
= 0x69
= 0x80
= 0xa5
= 0x5a
= 0xd2
"""The smallest identifiable unit on the bus, a byte or state change."""
=
=
= None
=
== :
=
== :
=
:
=
=
=
"""A collection of events between a SYNC and EOP (or similar)"""
= None
= None
=
=
=
= .
return
return is not None
"""A group of one or more packets that can be skipped if repeated."""
= 0x00
= 0x01
= 0x02
"""Determine if pkts starts with one of the known foldable combinations
Returns tuple: (combo_type, number_of_packets, unique_data)
unique_data is the subset of the combination of packet data
that is relevant to determining if packet combos are similar
enough to fold. For instance, every SOF packet has a
different frame number and CRC, so only the first byte
matters, while IN requests only match if all of their data
matches.
"""
return , 1,
>= 2 and > 1 and == 1 and \
. == and . == :
return , 2, . + .
return , 1,
"""Buffer of decoded UsbPackets that haven't been written to disk yet."""
=
=
= 0
= 0
=
= 0
=
return 0
# scan for the same packet combo, counting how many packets in
# a row can be skipped
= 0
, , =
# ignore SOF packets when folding
break
# stop folding on SOFs
break
+=
return
# only consume packets down to the maximum folding level,
# unless this is the end and we're flushing everything.
= 0
# see if the buffer starts with a known, foldable packet combination
, , =
=
# write the packet combo (or individual packet, if unknown)
=
+= 1
+= 1
= .
=
# if there are foldable duplicate packet combos, drop them
# and replace with a syslog packet
= .
=
= ..
=
+=
=
=
# consume CSV header
,,,, =
=
return
= # parse saleae CSV file into event objects
= # open pcap file and write mandatory headers
= None # currently processing packet
# initialize a buffer for processing packet combinations
=
# write any ready packets to pcap file
# fill in and queue new packets based on events
# complete any active packet
# record resets and errors as syslog packets
== :
# start a new packet
=
# continue an existing packet
# in case a final packet was incomplete, finish it
# flush all packets to pcap file
=
=