Blob Blame History Raw
#!/usr/bin/perl
#
# $Id$
#
# This script is only of interest for developers who want to analyze
# USB snoopy dumps of canon camera communication made with USB snoopy 1.8
# (http://benoit.papillault.free.fr/usbsnoop/). This script will *not*
# work with output of earlier versions (e.g. 0.13 aka 1.3).
#
# NOTE: All "interesting" URB's generate a line ending with "***", so the
#  command "urb-interpret foo.LOG | grep '\*\*\*$'" will generate a summary
#  of the log.
#

use strict;
use warnings;

my %canon_cmd = (
    "0x01 0x11 0x202" => "Get picture/thumbnail",
    "0x01 0x12 0x201" => "Identify camera",
    "0x03 0x12 0x201" => "Get time",
    "0x03 0x11 0x403" => "Upload data block",
    "0x04 0x12 0x201" => "Set time",
    "0x05 0x11 0x201" => "Make directory",
    "0x05 0x12 0x201" => "Change owner (old) or get owner (new)",
    "0x06 0x11 0x201" => "Remove directory",
    "0x06 0x12 0x201" => "Change owner (new)",
    "0x09 0x11 0x201" => "Disk info request",
    "0x0a 0x11 0x202" => "Flash device ID",
    "0x0a 0x11 0x201" => "Delete file (new)",
    "0x0a 0x12 0x201" => "Power supply status",
    "0x0b 0x11 0x202" => "Get directory",
    "0x0d 0x11 0x201" => "Delete file (old) or Disk info request (new)",
    "0x0e 0x11 0x201" => "Set file attribute",
    "0x0e 0x11 0x202" => "Flash Device ID (new)",
    "0x0f 0x11 0x201" => "Set file time",
    "0x13 0x12 0x201" => "Remote Camera control",
    "0x17 0x12 0x202" => "Download captured image",
    "0x18 0x12 0x202" => "Download captured preview",
    "0x1a 0x12 0x201" => "Unknown EOS D30 command",
    "0x1b 0x12 0x201" => "EOS lock keys",
    "0x1c 0x12 0x201" => "EOS unlock keys",
    "0x1d 0x12 0x201" => "EOS get body ID (old) or Get Custom Functions (new)",
    "0x1f 0x12 0x201" => "Get camera abilities",
    "0x20 0x12 0x201" => "Lock keys, turn off LCD",
    "0x21 0x12 0x201" => "Unknown newer command",
    "0x22 0x12 0x201" => "Unknown newer command",
    "0x23 0x12 0x201" => "Get EOS Body ID (new)",
    "0x24 0x12 0x201" => "Get camera abilities (new)",
    "0x25 0x12 0x201" => "Remote Camera control (new)",
    "0x26 0x12 0x202" => "Get captured image (new)",
    "0x35 0x12 0x201" => "Unknown newer command",
    "0x36 0x12 0x201" => "Unknown newer command"
);

my %control_subcmd = ( "0x00" => "Camera control init",
		       "0x01" => "Exit release control",
		       "0x02" => "Start viewfinder",
		       "0x03" => "Stop viewfinder",
		       "0x04" => "Release shutter",
		       "0x07" => "Set release params",
		       "0x09" => "Set transfer mode",
		       "0x0a" => "Get release params",
		       "0x0b" => "Get zoom position",
		       "0x0c" => "Set zoom position",
		       "0x0d" => "Get available shot",
		       "0x0e" => "Set custom func.",
		       "0x0f" => "Get custom func.",
		       "0x10" => "Get extended release params size",
		       "0x11" => "Get extended params version",
		       "0x12" => "Get extended release params",
		       "0x13" => "Set extended params",
		       "0x14" => "Select camera output",
		       "0x15" => "Do AE, AF, and AWB"
		     );

my %intcode = ( "00" => "unknown 00",
		"01" => "unknown 01",
		"02" => "unknown 02",
		"05" => "unknown 05",
		"08" => "Thumbnail descriptor",
		"0a" => "Capture status",
		"0c" => "Image descriptor",
		"0e" => "EOS flash write success",
		"0f" => "Capture complete",
		"20" => "initialization"
	      );

my ($file, %urb, %urbdata, %urbpipe);

if ($#ARGV == -1) {
    warn("Syntax: $0 file [file ...]\n");
    exit(1);
}

foreach $file (@ARGV) {
    open(FILE, "< $file") or die("Could not open file '$file' for reading: $!\n");

    my $line;
    my $byte_offset;
    my $dumping_urbdata = 0;
    my $dumping_setup = 0;
    my $current_urb = -1;
    my $current_function_type;
    my $buffer = 0;
    my $direction;
    my $current_lines;
    my $seq;
    my $timestamp;

    warn("FILE: $file\n");

    while ($line = <FILE>) {
	chomp($line);
	$line =~ s/\r//;	# Trim possible <CR> (from Windows)

	if ($line =~ />>>\s+URB (\d+) going down/) {
	    $current_urb = $1;
	    #warn("URB $current_urb down...\n");
	    $direction = "down";
	    # There may be a previous URB under way that was truncated.
	    $dumping_urbdata = 0;
	    $dumping_setup = 0;
	} elsif ($line =~ /<<<\s+URB (\d+) coming back/) {
	    $current_urb = $1;
	    #warn("URB $current_urb read\n");
	    $direction = "back";
	    # There may be a previous URB under way that was truncated.
	    $dumping_urbdata = 0;
	    $dumping_setup = 0;
	} elsif ( $line =~ /Diag Driver: Receive/
		  || $line =~ /isEchoRequest/
		  || $line =~ /isWakeUpRequest/
		  || $line =~ /filter/ ) {
	    next;
	} elsif ( $line =~ /^\[\d+ ms\] UsbSnoop/ ) {
	    $dumping_urbdata = 0;
	    $dumping_setup = 0;
	    next;
	} elsif ( $line =~ /^\d+\s+\d+\.\d+\s+UrbLink/ ) {
	    $dumping_urbdata = 0;
	    $dumping_setup = 0;
	    next;
	} else {
	    if ($current_urb > 0) {
		if ( $line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+0000:\s+/
		     || $line =~ /^(\d+)\s+(\d+\.\d+)\s+0000:\s+/
		     || $line =~ /^\s+00000000:\s+/ ) {
		    if ( !($dumping_setup) ) {
			#warn ( "dumping URB data\n" );
			$dumping_urbdata = 1;
			$current_lines = 0;
			$byte_offset = "0000";

			# start of urbdata, decide which buffer this is
			if ($urbdata{$current_urb}{buffer0} ) {
			    $buffer = "buffer1";
			} else {
			    $buffer = "buffer0";
			}

			#warn("URB $current_urb $buffer\n");

			# mark that the buffer goes here
			push(@{$urb{$current_urb}}, "[buffer $buffer]");
		    } else {
			$current_lines = 0;
			$byte_offset = "0000";
			$buffer = "setup";
			#warn("URB $current_urb setup info\n");

			# mark that the buffer goes here
			push(@{$urb{$current_urb}}, "[buffer setup]");
		    }
		}
		if ($dumping_urbdata) {
		    # Within data buffer
		    my $hex = "";
		    if ($line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/
			|| $line =~ /^(\d+)\s+(\d+\.\d+)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/
			|| $line =~ /^\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/ ) {
			$seq = $1;
			$timestamp = $2;
			$byte_offset = $3;
			$hex = $4;
			#warn ( "case 1: row $byte_offset of URB $current_urb: seq $seq time $timestamp data $hex\n" );
		    } elsif ( $line =~ /^\s+([\da-f]+):((\s[\da-f][\da-f])+)\s*$/ ) {
			$byte_offset = $1;
			$hex = $2;
			#warn ( "case 2: row $byte_offset of URB $current_urb\n" );
		    } elsif ( $line =~ /^(\d)+\s+(\d+\.\d+)\s+[a-f]:\s+$/ ) {
			$byte_offset = $3;
			$hex = $4;
			#warn ( "case 3: row $byte_offset of URB $current_urb\n" );
		    } else {
			#warn ( "case 4: not in data URB $current_urb\n" );
			if ($line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+([\da-f]+):\s+$/
			    || $line =~ /^(\d+)\s+(\d+\.\d+)\s+([\da-f]+):\s+$/ ) {
			    # every other line is this kind of line, data offset
			    $byte_offset = $3;
			    next;
			} elsif ( $line =~ /^\s+([a-f0-9]+):(( [a-f0-9][a-f0-9])+)$/ ) {
			    $hex = $2;
			} else {
			    if ( $line =~ /UrbLink/
				 || $line =~ /^(\d+)\s+(\d+\.\d+)\s*$/ ) {
				# this is not a urbdata line. reset.
				$dumping_urbdata = 0;
				next;	# unimportant line, just go to next
			    } else {
				warn("$file:$. Unknown data '$line' when dumping URB data\n");
				next;
			    }
			}
		    }
		    if ($hex) {
			if ( $urbdata{$current_urb}{op} eq "WRITE" ) {
			    # If this is an OUT transaction, try to decode
			    #  Canon command codes. These will be in two lines
			    #  of the buffer dump: the first, starting at offset
			    #  0000:; and the fifth, starting at offset 0040:
			    if (hex($byte_offset) == 0x00) {
				my @h = split(" ", $hex);
				if ( $#h < 8 ) {
				    warn ( "URB $current_urb has digits missing from cmd3: probable truncated packet\n" );
				    $urbdata{$current_urb}{cmd3} = "BAD";
				}
				else {
				    $urbdata{$current_urb}{cmd3} = sprintf("0x%02x", hex("$h[7]$h[6]$h[5]$h[4]"));
				}
			    } elsif (hex($byte_offset) == 0x40) {
				my @h = split(" ", $hex);
				if ( defined($h[4]) ) {
				    $urbdata{$current_urb}{cmd1} = "0x$h[4]";
				}
				if ( defined($h[7]) ) {
				    $urbdata{$current_urb}{cmd2} = "0x$h[7]";
				}
			    } elsif (hex($byte_offset) == 0x50) {
				if ( defined ( $urbdata{$current_urb}{cmd1} )
				     &&  defined ( $urbdata{$current_urb}{cmd2} ) ) {
				    if ( ( $urbdata{$current_urb}{cmd1} eq "0x13"
					 && $urbdata{$current_urb}{cmd2} eq "0x12")
					 || ( $urbdata{$current_urb}{cmd1} eq "0x25"
					      && $urbdata{$current_urb}{cmd2} eq "0x12") ) {
					my @h = split(" ", $hex);
					$urbdata{$current_urb}{subcmd} = "0x$h[0]";
				    }
				}
			    }
			} elsif ($urbdata{$current_urb}{pipe} eq "2" && hex($byte_offset) == 0x00 ) {
			    # An interrupt read; let's save the data for later decoding
			    $urbdata{$current_urb}{intdata} = $hex;
			} elsif ($urbdata{$current_urb}{op} eq "READ" && hex($byte_offset) == 0x40 ) {
			    my @h = split(" ", $hex);
			    if ( defined($h[0x4]) && defined($h[0x5])
				 && defined($h[0x6]) && defined($h[0x7]) ) {
				$urbdata{$current_urb}{usbid} = "(0x$h[0x7]$h[0x6],0x$h[0x5]$h[0x4])";
				$urbdata{$current_urb}{magic} = "$h[0x3]$h[0x2]$h[0x1]$h[0x0]";
			    }
			}

			if ( $current_lines < 65 ) {
			    push(@{ $urbdata{$current_urb}{ $buffer } }, hex_format($byte_offset, $hex));
			}
			$current_lines++;
			$byte_offset = -1;
			next;
		    }
		} elsif ( $dumping_setup ) {
		    my $hex = "";
		    if ($line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/
			|| $line =~ /^(\d+)\s+(\d+\.\d+)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/ ) {
			$byte_offset = $3;
			$hex = $4;
		    } elsif ( $line =~ /^\s+([\da-f]+):((\s[\da-f][\da-f])+)\s*$/ ) {
			$byte_offset = $1;
			$hex = $2;
		    } else {
			warn("$file:$. Unknown data '$line' when dumping URB setup data\n");
			next;
		    }
		    if ($hex) {
			# Store this for later dumping.
			if ( $current_lines < 65 ) {
			    push(@{ $urbdata{$current_urb}{setup} }, hex_format($byte_offset, $hex));
			}
			$current_lines++;
		    }
		    next;
		} else {
		    # Not within data buffer; parse other info
		    if ($line =~ /\sInterface\[0\]: Pipes\[(\d+)\] : PipeHandle\s+= 0x([\da-f]+)/) {
			# remember URB pipe
			$urbpipe{$2} = $1;
			warn("PIPE $2 = $1\n");
			print ( "Pipe $2 = $1" );
			if ( $1 == 0 ) {
			    print ( " (main)\n" );
			} elsif ( $1 == 2 ) {
			    print ( " (interrupt)\n" );
			} else {
			    print ( "\n" );
			}
		    } elsif ($line =~ /\sPipeHandle\s+= ([\da-f]+)/) {
			# check if we know this URB pipe
			if ( defined ( $urbpipe{$1} ) ) {
			    $urbdata{$current_urb}{pipe} = $urbpipe{$1};
			} else {
			    if ( $current_function_type =~ "CONTROL_TRANSFER" ) {
				$urbpipe{control} = $1;
				$urbdata{$current_urb}{pipe} = "<control>";
			    } else {
				#warn ( "Unknown pipe $1 from line $line" );
				$urbdata{$current_urb}{pipe} = "UNKNOWN";
			    }
			}
			#warn("URB $current_urb PIPE $1 = $urbdata{$current_urb}{pipe}\n");
		    } elsif ( $line =~ /SetupPacket\s+=\s*/ ) {
			#warn ( "found setup packet for URB $current_urb" );
			$dumping_urbdata = 0;
			$dumping_setup = 1;
			next;
		    }

		    if ( $line =~ /^--\s+URB_FUNCTION_([A-Z_]+):/
			 || $line =~ /^\d+\s+\d+\.\d+\s+--\s+URB_FUNCTION_([A-Z_]+):/
			 || $line =~ /^\d+\s+\d\d:\d\d:\d\d [AP]M\s+--\s+URB_FUNCTION_([A-Z_]+):/ ) {
			# Informing us of transfer type. Seven kinds have been seen.
			$current_function_type = $1;
			#warn ( "URB $current_urb is of function type $current_function_type" );
			if ( $current_function_type =~ "GET_DESCRIPTOR_FROM_DEVICE" ) {
			    # This is a read request, but lacks the "TransferFlags"
			    #  keyword.
			    $urbdata{$current_urb}{op} = "READ";
			}
		    }
		    elsif ($line =~ /.*\sTransferFlags.+USBD_TRANSFER_DIRECTION_(OUT|IN)/) {
			# OP in "coming back" is most reliable, and that one
			# is always after "going down" so we don't need to
			# check previous value here, just overwrite
			if ($1 eq "OUT") {
			    $urbdata{$current_urb}{op} = "WRITE";
			} elsif ($1 eq "IN") {
			    $urbdata{$current_urb}{op} = "READ";
			} else {
			    warn ( "Invalid transfer direction \"$1\"\n" );
			}
		    }
		    if ( $line =~ /\s+TransferBufferLength\s+=\s+([\da-f]+)/ ) {
			my $t = hex($1);

			if ( !defined($urbdata{$current_urb}{op}) ) {
			    warn ( "URB $current_urb length=$t, but don't know type\n" );
			}
			elsif ($urbdata{$current_urb}{op} eq "READ") {
			    # ok, this is read operation. info is
			    # always good.
			    $urbdata{$current_urb}{bytes} = $t;
			    #warn ( "URB $current_urb read $t\n" );
			} elsif ($urbdata{$current_urb}{op} eq "WRITE") {
			    if ($direction eq "down") {
				$urbdata{$current_urb}{bytes} = $t;
				#warn ( "URB $current_urb $urbdata{$current_urb}{op} $urbdata{$current_urb}{bytes}\n" );
			    }
			    # else, this information is unreliable
			}
		    }
		}
	    }
	}

	if ($line =~ /^(\d+)	([\w\d:\.]+?)	(.+?)\s*$/) {
	    # we want to trim down the line numbers and time

	    my $lineno = $1;
	    my $time = $2;
	    my $rest = $3;

	    $line = sprintf("%.5d %.3f  %s", $lineno, $time, $rest);
	} elsif ( $line =~ /^(\d+)\s+(\d+\.\d+)\s+(.+?)\s*$/ ) {
	    # we want to trim down the line numbers and time

	    my $lineno = $1;
	    my $time = $2;
	    my $rest = $3;

	    $line = sprintf("%.5d %.3f  %s", $lineno, $time, $rest);
	} elsif ( $line =~ /^\[(\d+) ms\]	(.+?)\s*$/ ) {
	    my $time = $1;
	    my $rest = $3;
	} else {
	    # replace tabs with two spaces to make lines shorter
	    $line =~ s/	/  /g;
	}

	if ($current_urb == -1) {
	    print("$line\n");
	} else {
	    push(@{$urb{$current_urb}}, $line);
	}
    }

    close(FILE);

    warn("\noutputting formatted data\n");

    my $n_skipped = 0;

    foreach $current_urb (sort numerically keys %urb) {

	# these are declared for readability
	my $cmd1 = $urbdata{$current_urb}{cmd1};
	my $cmd2 = $urbdata{$current_urb}{cmd2};
	my $cmd3 = $urbdata{$current_urb}{cmd3};
	my $pipe = $urbdata{$current_urb}{pipe};
	my $op = $urbdata{$current_urb}{op};
	my $length = $urbdata{$current_urb}{bytes};
	my $keep_alive = 0;

	if ( defined($op) ) {
	    if ( !defined($length) ) {
		warn ( "Missing operation length in URB $current_urb\n" );
	    } elsif ( defined($pipe) ) {
		if ( $pipe eq "2" && $length eq "0") {
		    $n_skipped++;
		    next;
		}
	    }
	    if ( $n_skipped ) {
		print ( "\n*** Skipped $n_skipped null interrupt reads ***\n" );
		$n_skipped = 0;
	    }
	    if ($op eq "READ") {
		if ( defined($length) ) {
		    if ( $pipe eq "2" ) {
			my $hex = "";
			if ( defined ( $urbdata{$current_urb}{intdata} ) ) {
			    $hex = $urbdata{$current_urb}{intdata};
			    my @h = split ( " ", $hex );
			    my $code = $h[4];
			    if ( defined ( $intcode{$code} ) ) {
				if ( hex($code) == 0x0a ) {
				    # Capture status: there's a subcode that tells us more.
				    my $subcode = hex($h[0x0c]);
				    if ( $subcode == 0x1c ) {
					printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: capture start ***\n\n", $length );
				    }
				    elsif ( $subcode == 0x1d ) {
					printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: capture finish ***\n\n", $length );
				    }
				    elsif ( $subcode == 0x1b ) {
					printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: flash wait ***\n\n", $length );
				    }
				    elsif ( $subcode == 0x0a ) {
					printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: photographic failure ***\n\n", $length );
				    }
				} else {
				    printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: $intcode{$code} ***\n\n", $length );
				}
			    } else {
				printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: unknown type $code ***\n\n", $length );
			    }
			} elsif ($length == 0x40) {
			    print("\n\n\n\n*** URB $current_urb possible CANON RESPONSE 0x%x bytes ***\n", $length);
			} elsif ($length == 0x01) {
			    print("\n\n\n\n*** URB $current_urb probably CAMERA INIT ***\n\n");
			} elsif ($length == 0x58 && defined ($urbdata{$current_urb}{usbid}) ) {
			    print("\n\n\n\n*** URB $current_urb probably camera initial response, USB ID=$urbdata{$current_urb}{usbid}, magic=$urbdata{$current_urb}{magic} ***\n\n");
			} else {
			    print ( "\n*** URB $current_urb interrupt read, but data are missing from log file ***\n\n" );
			}
		    } elsif ($length == 0x58 && defined ($urbdata{$current_urb}{usbid}) ) {
			if ( $urbdata{$current_urb}{magic} == 0x00000002 ) {
			    print("\n\n\n\n*** URB $current_urb possible CANON RESPONSE 0x%x bytes ***\n", $length);
			} else {
			    print("\n\n\n\n*** URB $current_urb probably camera initial response, USB ID=$urbdata{$current_urb}{usbid}, magic=$urbdata{$current_urb}{magic} ***\n\n");
			}
		    } elsif ($length == 0x01) {
			print("\n\n\n\n*** URB $current_urb probably CAMERA INIT ***\n\n");
		    } elsif ($length >= 0x40) {
			printf("\n\n\n\n*** URB $current_urb possible CANON RESPONSE 0x%x bytes ***\n", $length);
		    } else {
			print("\n");
		    }
		} else {
		    print("\n");
		}
	    } elsif ($op eq "WRITE") {
		if ( defined($length) ) {
		    my $cmd;
		    if ( defined($urbdata{$current_urb}{cmd1}) ) {
			$cmd = "$urbdata{$current_urb}{cmd1}";
		    } else {
			$cmd = "LOST";
		    }
		    if ( defined($urbdata{$current_urb}{cmd2}) ) {
			$cmd = "$cmd $urbdata{$current_urb}{cmd2}";
		    } else {
			$cmd = "$cmd LOST";
		    }
		    if ( defined($urbdata{$current_urb}{cmd3}) ) {
			$cmd = "$cmd $urbdata{$current_urb}{cmd3}";
		    } else {
			$cmd = "$cmd LOST";
		    }
		    if ( defined ( $length ) ) {
			if ($length >= 0x50) {
			    if ( $cmd eq "LOST LOST LOST" ) {
				warn ( "Command string is $cmd at URB $current_urb\n" );
			    }
			    elsif ($canon_cmd{$cmd}) {
				if ( $cmd eq "0x13 0x12 0x201"
				     || $cmd eq "0x25 0x12 0x201" ) {
				    if ( defined($urbdata{$current_urb}{subcmd}) ) {
					printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes", $length);
					if ( $control_subcmd{$urbdata{$current_urb}{subcmd}} ) {
					    print ( ": $control_subcmd{$urbdata{$current_urb}{subcmd}} ***\n\n" );
					} else {
					    print ( ". Unknown subcommand: $urbdata{$current_urb}{subcmd} ***\n\n" );
					}
				    } else {
					# Special case for newer (e.g. EOS 20D) protocol
					printf("\n\n\n\n*** URB $current_urb CANON COMMAND 'Get Power Status (newer)' ($cmd) 0x%02x bytes ***\n\n", $length);
				    }
				} else {
				    printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes ***\n\n", $length);
				}
			    } elsif ($cmd3 eq "0x201" or $cmd3 eq "0x202") {
				printf("\n\n\n\n*** URB $current_urb PROBABLE CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
			    } else {
				printf("\n\n\n\n*** URB $current_urb possible CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
			    }
			} elsif ($cmd3 eq "0x201" or $cmd3 eq "0x202") {
			    if ($canon_cmd{$cmd}) {
				printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes ***\n\n", $length);
			    } else {
				printf("\n\n\n\n*** URB $current_urb PROBABLE CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
			    }
			} else {
			    printf("\n\n\n\n*** URB $current_urb possible CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
			}
		    } elsif ($cmd3 eq "0x201" or $cmd3 eq "0x202") {
			if ($canon_cmd{$cmd}) {
			    printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes ***\n\n", $length);
			} else {
			    printf("\n\n\n\n*** URB $current_urb PROBABLE CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
			}
		    }
		} else {
		    warn ( "URB $current_urb write of unknown length" );
		    print ( "\n\n\n\n*** URB $current_urb write of unknown length ***\n\n" );
		}
	    }

	    if ( defined($length) ) {
		printf("%s 0x%x BYTES\n", $op, $length);
	    } else {
		printf( "%s *unknown* BYTES\n", $op );
	    }
	} else {
	    print ( "*** URB $current_urb has missing type info ***\n" );
	}

	if ($urbdata{$current_urb}{pipe}) {
	    print("*** URB $current_urb  PIPE $urbdata{$current_urb}{pipe}\n");
	} else {
	    print("*** URB $current_urb\n");
	}
	foreach $line (@{$urb{$current_urb}}) {
	    if ($line =~ /^\[buffer (buffer\d)\]$/) {
		my $b = $1;

		if ($b eq "buffer0" and $urbdata{$current_urb}{buffer1}) {
		    # this is buffer 0 but this URB has two buffers,
		    # this one is FOO (buffer contents before read typically)
		    # so we just skip it
		    print("[cut]\n");
		    next;
		}

		print("DATA:\n",
		      join("\n", @{ $urbdata{$current_urb}{$b} }),
		      "\n");
	    } elsif ( $line =~ /^\[buffer setup\]$/) {
		print("SETUP:\n",
		      join("\n", @{ $urbdata{$current_urb}{setup} }),
		      "\n");
	    } else {
		print("$line\n");
	    }
	}
    }
}

warn("\nDone\n\n");
exit(0);


# SUBROUTINES
# -----------

sub numerically { $a <=> $b };

sub hex_format
{
    my $byte_offset = shift;
    my $hex = shift;

    $hex =~ s/^\s*(.+?)\s*$/$1/;	# trim
    my @chars = split(" ", $hex);
    my $out_rad = "$byte_offset  $hex  ";

    # if not a full line, add whitespace
    if ($#chars < 15) {
	$out_rad .= " " x ((15 - $#chars) * 3);
    }

    # add ASCII
    my $ch;
    foreach $ch (@chars) {
	my $i = hex($ch);
	if ($i >= 32 and $i < 127) {
	    $out_rad .= sprintf("%c", $i);
	} else {
	    $out_rad .= ".";
	}
    }

    return $out_rad;
}