I am the proud owner of Stamps.com Model 510 5lb digital scale. It is a nice little scale which works very well (much better than Stamps.com service itself) while attached to my workstation. The scale doesn’t have a display making any kind of standalone use difficult. However, since the scale is a USB HID device reading data from it should be as easy as from a joystick and Arduino board should be adequate to provide a display function for it. To test this theory I made a simple setup consisting of Arduino UNO, USB Host shield and HD44780-compatible LCD display. I also wrote a small sketch which polls the scale and outputs the weight. The secondary objective of this project was to demonstrate LCD support in USB Host shield library.
For this project I used the following:
- An Arduino board. Standard size board, such as UNO, Duemilanove or Leonardo, will work
- USB Host Shield
- Toshiba HD44780-compatible LCD display, in 16×1 or 16×2 configuration. If you’re planning to use this sketch for something else, like data logging, the display is optional – all output from the scale is repeated to the serial port
- Stamps.com 5lb digital scale. Scales are standard HID devices with usage table 0x8d, therefore, scales from other brands may work as well with no or minimal modifications to the code
- USB Host library
The example code is also hosted at github, as well as in ‘examples’ section of the library under ‘HID’. It has been tested with Arduino IDE version 1.0.5.
In this project, the LCD is connected to the shield’s GPOUT pins, as documented in max_LCD.h header file. In addition to data lines, 5V and ground must also be connected to the shield’s 5V and GND terminals; the RW pin must be grounded – I do it on the LCD itself. In order to see the characters, the display must be biased – a 5K-10K pot with wiper on Vo and other two pins on 5V and ground will provide contrast adjustment.
Now it’s time to load a sketch, connect the scale to the USB port of the shield and open a terminal. If everything is good, the following will be printed:
Start Weight: 0.00 oz |
If a LCD is (properly) connected, similar information will be output to it also.
In addition to the weight, several diagnostic messages will be printed, indicating, for example, excessive weight. Below, I’m placing several books on a a scale, one by one, until a limit is reached:
Weight: 41.90 oz Weight: 43.40 oz Weight: 43.50 oz Weight: 70.40 oz Weight: 95.60 oz Max.weight reached |
As I said before, the scale is a HID device and it works similarly to any other HID device – after initialization it starts responding to requests from the host reporting its state. The code is very similar to one written to poll a Logitech joystick, the main differences being (obviously) report data structure and parsing as well as using an LCD for the output along with the terminal.
I recently started a github repository containing USB device traces. If you’re curious about the protocol details, this trace contains scale initialization performed by a Windows 7 PC, as well as report polling. For the rest, the HID support in the library is designed in such a way that almost everything happens automatically, the only device-specific piece of code being the parser. Let’s go through the steps to change a joystick parser into a scale parser.
The scale report is 6 bytes. Below is a report returned by empty scale. The meaning of the bytes, from left to right, is as follows:
03 04 0B FF 00 00 |
- 03 – this is the report ID, always 3
- 04 – status, in this case meaning the report containing correct weight
- 0B – unit of measurement, in this case ounces
- FF – exponent to the base of 10 to make a multiplier for the weight value contained in the next 2 bytes. It is a signed integer, in this case -1, which means the weight needs to be divided by 10
- 00 00 – weight, in this case zero
Now let’s place something on a scale – like a 4.5oz phone. The report changes – the weight is now 0x2D
or decimal 45.
03 04 0B FF 2D 00 |
Let’s now put a really big weight thus overloading the scale. We can see that the status has changed to 0x06
meaning overweight condition. In this case, the reported weight is invalid.
03 06 0B FF D5 04 |
More information about scale report format is available at usb.org website in HID section.
For our parser, this data structure is defined in scale_rptparser.h
file and looks like this:
/* input data report */ struct ScaleEventData { uint8_t reportID; //must be 3 uint8_t status; uint8_t unit; int8_t exp; //scale factor for the weight uint16_t weight; // }; |
The method which gets called every time a report has changed due to change in weight or status, is called ScaleEvents::OnScaleChanged()
. This is how it looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | void ScaleEvents::OnScaleChanged(const ScaleEventData *evt) { pLcd->clear(); pLcd->home(); pLcd->setCursor(0,0); if( evt->reportID != 3 ) { const char inv_report[]="Invalid report!"; Serial.println(inv_report); LcdPrint(inv_report); return; }//if( evt->reportID != 3... switch( evt->status ) { case REPORT_FAULT: Serial.println(F("Report fault")); break; case ZEROED: Serial.println(F("Scale zero set")); break; case WEIGHING: { const char progress[] = "Weighing..."; Serial.println(progress); LcdPrint(progress); break; } case WEIGHT_VALID: { char buf[10]; double weight = evt->weight * pow( 10, evt->exp ); Serial.print(F("Weight: ")); Serial.print( weight ); Serial.print(F(" ")); Serial.println( UNITS[ evt->unit ]); LcdPrint("Weight: "); dtostrf( weight, 4, 2, buf ); LcdPrint( buf ); LcdPrint( UNITS[ evt->unit ]); break; }//case WEIGHT_VALID... case WEIGHT_NEGATIVE: { const char negweight[] = "Negative weight"; Serial.println(negweight); LcdPrint(negweight); break; } case OVERWEIGHT: { const char overweight[] = "Max.weight reached"; Serial.println(overweight); LcdPrint( overweight ); break; } case CALIBRATE_ME: Serial.println(F("Scale calibration required")); break; case ZERO_ME: Serial.println(F("Scale zeroing required")); break; default: Serial.print(F("Undefined status code: ")); Serial.println( evt->status ); break; }//switch( evt->status... } |
- Lines 4-6 – here I’m clearing the LCD screen to prepare for the new output
- Line 8 – I’m checking the report ID and return an error if the ID is wrong
- Line 19 – the switch statement handling status field of the report. Almost every status means some kind of error so I just print the error message and return
- Line 37 – this status means the valid weight is present in the report. I first calculate the real weight (line 40) and then output it
Now, Where does this method actually gets called? In the same file several lines earlier from a method called Parse()
. HID driver periodically calls this name when it polls the HID device.
If you don’t need to write your own parser but just want to change the behaviour of this example, the only place which needs to be modified is OnScaleChanged()
method.
Next I want to show how these classes are instantiated in the main sketch. Here’s the fragment of the definition section:
1 2 3 | Max_LCD LCD(&Usb); ScaleEvents ScaleEvents(&LCD); ScaleReportParser Scale(&ScaleEvents); |
The first line instantiates a LCD class and it is always defined like that. Second line instantiates ScaleEvents
– the class where the most important method OnScaleChanged()
resides. A pointer to LCD is passed to it. The last line define a report parser instance and pointer to ScaleEvents
is passed to it.
The following statement inside a setup()
links report parser to the HID driver. It is the last link in the chain – from this point, everything just starts working.
if (!Hid.SetReportParser(0, &Scale)) ErrorMessage<uint8_t>(PSTR("SetReportParser"), 1 ); |
For better understanding I suggest opening the joystick code mentioned in the beginning of the article and compare it to the scale example. Don’t try to understand every line, just pay attention to code elements explained above. If anything in the code is not clear please leave a comment and I’ll try to explain it better.
Oleg.
Hi, Oleg
Have you ever provided some sketches that support more than 1 HID devices at same time? E.g. 1 Barcode scanner and 1 keyboard, which works great individually, but not works normally when together.
I don’t have this, sorry.
Great write up! Thanks for doing this work. I think I will make one two 🙂
Hi There Oleg,
Is it possible to ‘poll’ the scale? How quickly can you get changes in weight from the scale?
The Usb.Task() does the polling,
OnScaleChanged()
will be called if scale state changes. In general, scales are slow devices so one reading every 1-2 seconds is probably the fastest rate you can expect.Hello
Could the USB host shield talk with more then one scale?
Looking to make a scale system with 4 scales to measure cross weight of an RC car like they do in full scale cars racing.
Thanks
This should be possible, just use a hub.
I had just purchased a USB Host Shield 2.0 via Citibank credit card. Your webpage keep complaining that my billing address does not match billing address of cardholder, which is definitely not the case. I received SMS from Citibank informing me the transaction is successful, so I am not sure what to do now, did I get the item purchased successfully?
I already sent an email with details of transaction but no reply after 5 days. Sorry to post it here.
I’m sorry you’re having troubles. I have 2 questions:
a) Was it a US credit card? International credit card transactions don’t work very well, try using Paypal instead. If you believe your card has been charged, get the transaction number from your bank so I can investigate it on my side
b) What was the e-mail address you’ve sent the details to? Try one you received the registration info from or one listed in the “About” section.
I am using an international credit card, and the email was sent to general@circuitsathome.com
Anyway, so far the credit card doesn’t show any sign of being charged, I just received the SMS but does not see it get billed. I will contact you if it really get charged. I had purchased another set via TKJ electronics, and it works well. Thank you very much for the reply.
Thank you for your work.
I think it’s one of the simplest an cheapest way to get a precise weight from a scale (amplify the wheastone bridge signal easy but not precise, hacking lcd precise but you need time and oscilloscope)
Just for the records your code is also compatible with dymo M25.
I have bought one from ebay
I haven’t modified a single line of code and it works!
An other remark it’s not possible to get negative weight as the scale does not send negative weight through the usb.
You saved me lot of time!
thank you
Thank you for letting me know. I’m looking at the same scale for another project :-).
Hi thanks for all your work ..
Im trying to automate my home brewery 😉 and am using scales with a usb/serial output similar to yours though mine spit out ascii text.
based on the following description of my output string am i on the right track with the following edits to your sources
Captured output:
0UNG/W+ 84.55kg
0UNG/W+ 83.65kg
0UNG/W+ 83.05kg
0UNG/W+ 72.30kg
0UNG/W+ 64.15kg
0UNG/W+ 43.70kg
0UNG/W+ 23.05kg
0UNG/W+ 8.70kg
0UNG/W+ 7.85kg
0UNG/W+ 6.70kg
so
Byte 1 = “0” scales ID
Byte 2 = “S” stable “U” Unstable
Byte 3 = “N” No Error
Byte 4,5,6 = “G/W” Gross Weight
Byte 7 = “+” or “-” (00 is positive..)
Byte 8,9,10,11,12,13 NNN.NN Where N = 0to9 (float)
Byte 14,15 = Chosen Units abrv (Kg,lb,oz, lb-oz) will focus on Kg string may be longer if lboz selected ?? more testing needed..
Changes to make to scale_rptparser.h are..
/* Scale status constants */
#define STABLE 0x53 //”S” for stable
#define UNSTABLE 0x55 // “U” Unstable
/* input data report */
struct ScaleEventData
{
uint8_t reportID; // scale ID – should be “A”
uint8_t status; // Stability “S” stable “U” unstable
uint8_t sError; // Scale Error “N” no error ? no clue to what errors could be??
char8_t[3] wType; // Weight Type “G/W” (Gross weight) expected value
float16_t weight; // weight value
char8_t[2] units; // weight unit Kg ?? may be more characters need scales to test??
};
—————————————————-
Basic Changes to make to scale_rptparser.cpp
void ScaleEvents::OnScaleChanged(const ScaleEventData *evt)
{
double weight = evt->weight // double? float? same thing on an arduino 😉
Serial.print(F(“Weight: “));
Serial.print( weight );
Serial.print(F(” “));
Serial.println(evt->unit);
}
Many thanks in advance..
And as soon as i post i spot my error.. DOH! under 10kg the weight is less than 6 bytes, which throws out the further mapping
As my input is a string could i get away with..
struct ScaleEventData
{
char8_t[howeverlong] scaleReport
};
and decode in OnScaleChanged with lots of lovely string delimiting…
you can probably tell im not in your class when it comes to code 😉
I can have negative weights and will have ..
What are you trying to accomplish? If you want to suppress everything before the actual weight I would read the input until a space is encountered and then print the rest.
the mass value is the key data but also other aspects such as its stability and error status will be queried eventually.
for now it will be good to get data out i initially overlooked the master slave aspect of the usb protocol as its just a medium for peer to peer serial connections,, Whoops! Yes i had a usb header wired upto Rx1 and Tx1 on a mega board for a few evenings 😉
Its all for semi automation of a nano brewery, heating and transfering specific masses of liquor thru the 3 vessel system on schedule.
the scales will sit under the second vessel measuring the mass both in and out.
Update.. after a bit of editing, a lot of head scratching and a bit of backtracking through the articles, i slowly worked out i wanted to edit the simple acm terminal sketch instead..
now have a lil sketch spitting out signed floating point numbers from the scales.
Thank you for publishing all this info..