Using a magnetic card reader with jQuery
Posted on by GaretJax, filed under Web applications
Lately I was playing with a magnetic stripe card reader in order to develop an application to manage the members registration and identification for our university students union.
I tried to differentiate the input source of the reader from the keyboard using some low-level USB library, but then found out that the device – which has a simple HID interface – was already taken over by the kernel and the only way to avoid it was (as far as I know) to write a kernel extension.
Not having enough knowledge to write an extension of this type, I rapidly abandoned the idea to write the application in Python, Java or similar and got down the Web-Application road. In this article I’ll try to illustrate and explain the code I thrown together to use the device with the jQuery javascript library.
How the reader works
Being identified as a simple HID, the reader does not need a specific driver, and the content of any card you swipe through it is sent to the active application as if the user tapped the respective keys on the keyboard.
This is mainly an advantage, but if you want to control the device directly, it can become a mess to set up. For this article I went down the easy way and dealt with the device as if it were a keyboard.
The other important thing to know about the device is the format used to send the data.
Each magnetic stripe can have two or three tracks on it and the data on each track is sent with a track-dependent char prefixed to it (called Track ID) and with a track-independent char as suffix (called End sentinel). The format is Track ID + Data + ES + ENTER for a successful read and Track ID + 'E' + ES + ENTER if a read error occurred.
The following table resumes the different Track IDs (you can also refer to the PDF linked at the top of the page):
| Track | 1 | 2 | 3 | Error |
|---|---|---|---|---|
| Track ID | % |
; |
+ |
é |
| End sentinel | _ |
|||
Note that the end sentinel sent by my device differs for some strange reason from the one indicated in the documentation (which is a ?).
Another not documented behavior is the format of the data if a read error occurred before the track was identified. In this case, the devices sends the common error code (E) prefixed with the é Track ID.
Detecting input from the reader
The input from the reader can easily be detected with a general keypress event. If your application uses the card reader as input device or does not need to detect if the input actually comes from the keyboard or from the reader, a simple check of the Track ID ad and the end sentinel suffices to achieve the goal.
If instead – as it was the case for me and probably for most of the applications – there is the need to differentiate keyboard and card reader input, an easy solution is to measure the time between two successive keypress events and to cancel the reading if the delay is too long.
This simple test page measures the time between two events and provides you with the detailed results and the final average of all delays. In the case of my reader, I measured an average delay of 40 [ms].
The code
I throw together a simple Javascript class to detect and handle the input of the card reader based on jQuery events. The code can be found on GitHub and is licensed under the MIT license.
The usage is very simple, the first step consists to include the jQuery library and the reader class in your HTML page. Then you can initialize the reader and provide some callbacks which will handle the input:
<!-- Include the scripts -->
<script type="text/javascript" charset="utf-8" src="scripts/jquery-1.3.2.js"></script>
<script type="text/javascript" charset="utf-8" src="scripts/CardReader.js"></script>
<!-- Initialize the reader -->
<script type="text/javascript">
jQuery(function () {
// Create a new reader instance
var reader = new CardReader();
// Feed it an object to observe (this could also be a textbox)
reader.observe(window);
// Errback in case of a reading error
reader.cardError(function () {
alert("A read error occurred");
});
// Callback in case of a successful reading operation
reader.cardRead(function (value) {
$('form input#card_number').val(value);
$('from').submit();
});
});
</script>
The class also provides validation hooks. A validation hook is a function which is executed by the reader class before it fires the cardRead event. If all the validation hooks return true, then the value is correct and the event is fired, if instead an hook returns false, then the cardError event is fired:
// Add a new validation hook to the reader
reader.validate(function (value) {
// Tests if the value is a 4-digit long number
var pattern = new RegExp(/^\d{4}$/);
return pattern.test(value);
});
// Multiple hooks can be added, they get all executed
reader.validate(function (value) {
// Tests if the value is below 5000
return parseInt(value) < 5000;
});
Ignoring read errors
During the development of the subsequent part of the application I noticed that many times, a successful read, is followed by a read error, causing the errbacks1 to get executed straight after the callbacks.
To prevent this behavior (caused by the card reader itself), I added a second timeout which starts after a successful read. If a read error is encountered before the expiration of the timeout, it is ignored and not dispatched.
TODOs and possible improvements
- Remove all calls to
console.logfrom the code - Detect the three different Track IDs and provide the callbacks with enough data to act differently based on the track number. ATM the class detects only the first Track and fires all callbacks. The callbacks has no means to know from which track the input was read. Adding this little feature is pretty easy and straightforward;
- Re-fire the preceding
keydownevents if the read operation times-out If the user enters a Track ID by pressing the respective key on the keyboard, the class begins to read and then times-out, consuming the user generated event. This means that a user isn’t able to type a Track ID directly into a textbox or another input; - Use jQuery custom event triggering instead of my simple array-based dispatching.