Reading a Stamps.com USB Scale from C#

An innocuous, cheap looking plastic postal scale.

I’ve managed to use Stamps.com’s PDK to integrate Skiviez’s USPS shipping down to a pretty automatic process. First, you dump a package onto an old serial scale. Second, you scan the order number barcode. Our software (which is constantly polling that scale) reads the weight, loads the order, and passes this information off to the Stamps.com COM object (try saying “Stamps.com COM object” without sounding like an idiot; this is why I hate companies with “.com” in their name). Then, third, you usually just have to smash the Enter key and a delivery label comes shooting out of the printer. Great.

But we haven’t been using the little USB scale that was supplied with our Stamps.com account; instead, the software has been reading from an old serial scale that we got from UPS. Why? I knew how to read data from the serial scale (which is simple enough: write a newline to the serial port and get an ASCII string with the weight back), but the Stamps.com USB scale was a black box. First, I had no idea how even to begin to access data from a USB device in C#. Second, I wouldn’t know how to read data coming back from the scale even if I did.

Here’s my story of how I figured this out. I’m sure that there are more intelligent ways that I could have done this; if there are, I’d love to hear from you.

First, I sat down at my computer with Device Manager open and plugged in the scale. I notice that it appeared in Windows as a “USB Human Interface Device.” Now that’s interesting, I thought to myself. In all my years of futzing around with Windows, I see things pop up as a USB Human Interface Device all the time. I wonder what in the blue hell that actually means?

A quick trip to Google reveals that devices that are HIDs can send and receive data according to a common specification that is detailed in a several hundred page tome that I would care not to read. That means that the operating system can provide one generic HID driver that all of these devices can share. That’s a relief because it means that I don’t need to learn how to interact with some low-level, custom driver that was written specifically for this USB scale.

Since it appears that there is no native .NET way to interact with these HID devices, however, I began searching for a pre-built library. I struck gold with Mike O’Brien’s USB HID library, which has an easy-to-use API that enabled me to complete this project without having any clear idea of what exactly I was doing. These HID devices can send “reports” according to some common protocol; this library lets me read the reports, but figuring out the content of the report is still up to me to figure out.

So, having the scale hooked up, and using the example code on Mike O’Brien’s page, I hammered out some code that looked something like the following (I looked up the product and vendor ID by using the Properties pane in Device Manager):

HidDeviceData inData;
HidDevice[] hidDeviceList;
HidDevice scale;
 
hidDeviceList = HidDevices.Enumerate(0x1446, 0x6A73);
 
if (hidDeviceList.Length > 0)
{
	int waitTries;
 
	scale = hidDeviceList[0];
	waitTries = 0;
 
	scale.Open();
 
	if (scale.IsConnected)
	{
		inData = scale.Read(250);
 
		for (int i = 0; i < inData.Data.Length; ++i)
		{
			Console.WriteLine("Byte {0}: {1:X}", i, inData.Data[i]);
		}
	}
 
	scale.Close();
	scale.Dispose();
}

At this point, I was fairly flabbergasted because I actually got a reasonably small amount of data back:

Byte 0: 0x3
Byte 1: 0x4
Byte 2: 0xB
Byte 3: 0x0
Byte 4: 0x38
Byte 5: 0x0

Unfortunately, I had no idea what this data meant. So I stared at the numbers. During this test, I had been weighing my iPhone, and the LCD display on the scale was displaying 5.6 ounces.

Hmmm, I thought. I’m pretty much a moron when it comes to hexadecimal, so let me convert these numbers to decimal and see if they make any more sense to me. Here goes:

Byte 0: 3
Byte 1: 4
Byte 2: 11
Byte 3: 0
Byte 4: 56
Byte 5: 0

Well, that didn’t help a whole–wait a minute! Byte 4 says “56″ and there are 5.6 ounces on the scale? Coincidence? I think not!

So I added a Sharpie on top of my iPhone and ran the application again. 7.8 ounces and byte 4 says 78.

Like any good engineer, I think to myself, if it happens three times, it must be true! Sure enough, replacing the items with a dead hard drive that was laying on my desk resulted in 12.3 ounces == 123 in byte 4.

Great. So byte 4 is returning the weight in tenths of an ounce. So all I have to do is take byte 4, move the decimal point once to the left, and there I have it. But what happens when byte 4 overflows? To discover this, I pile up everything onto the scale–iPhone, Sharpie, and two dead hard drives–and the scale displays 2 lb 13.5 oz, or 37.5 oz. The data returned is

Byte 0: 3
Byte 1: 4
Byte 2: 11
Byte 3: 0
Byte 4: 119
Byte 5: 1

This doesn’t seem to make a lick of sense, until I noticed that byte 5 is now displaying a 1 where it always had returned a 0 before. I take a wild guess: if byte 4 is returned tenths of an ounce, could byte 5 be an overflow bit? 1 * 256 = 256 + 119 = 375 … yes! 37.5 ounces!

What happens when byte 5 overflows? Who knows–the scale is only rated for 25 lb, and overflowing bytes 4 and 5 would be like, what, 4,112 lb? Let’s not find out.

After some more twiddling, I discovered that byte 1 returns “4″ when the scale is stable (e.g., not bouncing up and down, trying to figure out the weight) and “not 4″ when it is sliding. Therefore, my code to read from a Stamps.com Model 2500i scale using Mike O’Brien’s library is as follows:

private void GetStampsComModel2500iScaleWeight(out decimal? ounces, out bool? isStable)
{
	HidDeviceData inData;
	HidDevice[] hidDeviceList;
	HidDevice scale;
 
	isStable = null;
	ounces = null;
 
	hidDeviceList = HidDevices.Enumerate(0x1446, 0x6A73);
 
	if (hidDeviceList.Length > 0)
	{
		int waitTries;
 
		scale = hidDeviceList[0];
		waitTries = 0;
 
		scale.Open();
		// For some reason, the scale isn't always immediately available
		// after calling Open(). Let's wait for a few milliseconds before
		// giving up.
		while (!scale.IsConnected && waitTries < 10)
		{
			Thread.Sleep(50);
			waitTries++;
		}
 
		if (scale.IsConnected)
		{
			inData = scale.Read(250);
			ounces = (Convert.ToDecimal(inData.Data[4]) +
				Convert.ToDecimal(inData.Data[5]) * 256) / 10;
			isStable = inData.Data[1] == 0x4;
		}
 
		scale.Close();
		scale.Dispose();
	}
}

I still have no idea what bytes 0, 2, or 3 signify, but I’ve followed the first rule of coding: when it’s working, stop coding.

I hope this helps somebody take a USB scale that Simply Does Not Work and turn it into one that Does. Good luck!