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
    
    
     = 
    
    
    
    
    
     =