Skip to main content

A USB Mystery

TL;DR: got some hardware that didn't have a driver I could use. Dived into some packet captures, learned some USB, wrote some code.

I've been on a mini quest to instrument a bunch of real-life things, lately.

One thing that's been on my mind is "noise" around my house. So, after a very small amount of research, I bought a cheap USB Sound Pressure Level ("SPL") meter. (I accidentally bought the nearly-same model that was USB-powered only (it had no actual USB connection), before returning it for this one, so be careful if you happen to find yourself on this path.) Why not use a regular microphone attached to a regular sound device? Calibration.

When the package arrived, and I connected it, I found that it was not the same model from my "research" above. I was hoping that many/most/all of these meters had the same chipset. So now I had a little bit of a mystery: how do I get data from this thing?

I managed to find it in my system devices (on Mac; I'd have used lsusb on Linux—this thing will eventually end up on a Raspberry Pi, though):

❯ system_profiler SPUSBDataType
(snip)
WL100:

  Product ID: 0x82cd
  Vendor ID: 0x10c4  (Silicon Laboratories, Inc.)
  Version: 0.00
  Speed: Up to 12 Mb/s
  Manufacturer: SLAB
  Location ID: 0x14400000 / 8
  Current Available (mA): 500
  Current Required (mA): 64
  Extra Operating Current (mA): 0
(snip)

So, I at least know it actually connects to the computer and identifies itself. But I really had no idea where to go from there. I found that Python has a PyUSB library, but even with that set up, my Mac was unhappy that I'd try accessing USB devices from userspace (non-sudo). I found there was also another way to connect to devices like this, over "HID", which is the protocol normally used for things like the keyboard/mouse, but is over-all a simpler way to connect things.

The vendor supplied software on a mini-CD. Hilarious. There was also a very sketchy download link for Windows-only software. I have a Windows box in the networking closet for exactly this kind of thing (generally: testing of any sort). So, I went looking for some USB sniffing software, and a friend remembered that he thought Wireshark could capture USB. Perfect! I'd used Wireshark many times to debug networking problems, but never for USB. This was a lead nonetheless.

I fired up the vendor's software and connected the SPL meter:

Okay. It's ugly, but it seems to work. This app looks like it's from the Win32 days, and I thought that was no longer supported… but it works—or at least seems to. I asked Wireshark to capture on USBPcap1, and waited until I saw it update a few times. Disconnected the capture, saved the pcap session file, and loaded it into Wireshark on my main workstation. Unfortunately, I didn't have much of an idea what I was looking at.

I could, however, see what looked like the conversation between the computer (host), and the SPL meter (1.5.0). The was marked USBHID (as opposed to some other packets marked only USB), so this was a great clue:

The led to some searches around GET_REPORT and USB/HID/hidapi. Turns out that USB HID devices have "endpoints", "reports", and a lexicon of other terms I could only guess about. I didn't plan to become a full USB engineer, and was hoping I could squeeze by with a bit of mostly-naïve-about-USB-itself-but-otherwise-experienced analysis.

Eventually, I figured out that I can probably get the data I want by asking for a "feature report". Then I found get_feature_report in the Python hidapi bindings.

This function asks for a report_num and max_length:

def get_feature_report(self, int report_num, int max_length):
    """Receive feature report.

    :param report_num:
    :type report_num: int
    :param max_length:
    :type max_length: int
    :return: Incoming feature report
    :rtype: List[int]
    :raises ValueError: If connection is not opened.
    :raises IOError:
    """
    

These two values sound familiar. From the Wireshark capture:

Now I was getting somewhere. Let's use that decoded ReportID of 5 and a max_length (wLength) of 61.

import hid
import time

h = hid.device()
# these are from lsusb/system_profiler
h.open(0x10C4, 0x82CD)

while True:
    rpt = h.get_feature_report(5, 61)
    print(rpt)
    time.sleep(1)

This gave me something like:

[5, 97, 239, 60, 245, 0, 0, 1, 85, 0, 0, 1, 44, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[5, 97, 239, 60, 246, 0, 0, 1, 99, 0, 0, 1, 44, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[5, 97, 239, 60, 247, 0, 0, 1, 172, 0, 0, 1, 44, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[5, 97, 239, 60, 248, 0, 0, 3, 63, 0, 0, 1, 44, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[5, 97, 239, 60, 249, 0, 0, 2, 168, 0, 0, 1, 44, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[5, 97, 239, 60, 250, 0, 0, 1, 149, 0, 0, 1, 44, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[5, 97, 239, 60, 251, 0, 0, 1, 71, 0, 0, 1, 44, 5, 20, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

I played around with this data for a bit, and eventually noticed that the 8th and 9th (rpt[7:9]) values were changing. Sure enough, if I made a noise, the 9th value would change, and if it was a loud noise, the 8th value would also change:

1, 85
1, 99
1, 172
3, 63
2, 168

I was about to start throwing data into a spreadsheet when I made a guess: what if that's a 16 (or 12…) bit number? So, if I shift the first byte over 8 bits and add the second byte…

(1 << 8) + 85 == 341
(1 << 8) + 99 == 355
(1 << 8) + 172 == 428
(3 << 8) + 64 == 831
(2 << 8) + 167 == 680 

The meter claims to have a range of 30dBA to 130dBA, and it sits around 35dBA when I'm not intentionally making any noise, in my office with the heat fan running. Now I'm worried that it's not actually sharing dBA numbers and maybe they're another unit or… wait… those ARE dBA numbers, just multiplied to avoid the decimal! 34.1, 35.5, 42.8, 83.1, 68.0

Got it!

Anyway, I wrote some (better) code to help read this data, on Python: scoates/wl100 on GitHub. Let me know if you use it!