Jonathan's Blog

Using a magnetic card reader with jQuery

Posted on by , 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):

Tab 1: Track IDs and sentinel
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.log from 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 keydown events 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.

  1. Being an active user of Twisted, I’m calling a callback attached to an error event an errback

  • Nirmal
    Thank you for the reply. I achieved it by checking whether any input element is in focus and inserted the start sentinel value to that element through the timeout event.

    // extend the jquery to have a custom selector.
    // this is used to check if an element has its focus.
    // usage: $("input:focus").doStuff();

    jQuery.extend(jQuery.expr[':'], {
    focus: "a == document.activeElement"
    });


    And modified the first timeout event to be like this:

    this.timer = setTimeout(function () {
    ob.started = false;
    ob.finished = false;
    ob.isError = false;
    ob.input = "";
    $("input:focus").val($("input:focus").val() + String.fromCharCode(ob.track_start));
    }, this.timeout);


    This solved the problem if someone tried to enter % in any of the input elements in the page.
  • Nirmal
    I am having a situation which this code makes difficult. Our login page accepts either username+password or a card swipe. Whenever I try to key in the % symbol (start sentinel) in the username or password box, it won't show up. Instead the observer is activated and it's preventing the % entry. This will be a problem because special characters are usually allowed in passwords.

    Is there any way to workaround this problem? Thank you for your time.
  • You can add the CardReader listener to the body of the page and add a KeyListener on the input which stops the event propagation. This way if the user wants to use the card reader the input shall not be selected.

    The more elegant solution would be to implement point 3 of he TODOs and refire the event if the operation times-out
  • Nirmal
    My fault, I didn't notice the constructor.
    Thanks for the update.
  • Ursula
    Nice code, can't get it working though.
    Is the HTML (i suppose?) above enough? Shouldnt there be any HTML elements to test it (like body?)
    Also, how do I replace the track end "_" with an ENTER key (Asc 13) ?
    As it is now, i have a scanner emulated as a normal keyboard, scanning text or numbers - nothing happends on the .html though, not even when typing with manual keyboard... ideas?
  • Here you go: http://gist.github.com/502257
    As you can see the body contains a form with an invisible field, the script will show you an alert box if a read error occurs or submit the page with the results (the card content can then be read from the URL). Also turn on the console output in your web browser to receive debugging messages from the class itself. ;-)
  • Ursula
    Thanks for the reply Jonathan.
    "The HTML above is not enough, you should already have a complete working page and the snippet is the scripting part to add to capture the reader input."
    Could you elaborate? Would it be enough to make a <div></div> and call the function within? Some minimalistic test page code would be appreciated (I'm new to jQuery....)
    Btw, the key-delay test page was just amazing.
  • Hi Ursula,
    The HTML above is not enough, you should already have a complete working page and the snippet is the scripting part to add to capture the reader input. Also remember that it works only if the window has focus!
    You can test your reader at some extent using the linked test page: http://static.garetjax.info/magnetic-card-jquery/key-delay.html

    As for the question about the end sentinel, there are no code lines to modify, it suffices to pass the track_end argument to the CardReader constructor (I think a value of "\r" should do the trick).

    Hope it helps ;-)
  • Thanks for the feedback!
    I should really expand a little the script and wrap it in a jQuery plugin, maybe with a little more documentation too...
  • Nirmal
    For someone who is wondering why this code doesn't work as it is, you have to edit the CardReader.js file to specify the correct track end character:

    As Garet pointed out, his reader device throws an underscore for track end:
    this.track_end = track_end || "_";

    But most of the devices in the market decode the track end as a question mark:
    this.track_end = track_end || "?";

    You must be good to go with this change.
  • Nirmal
    Mr. Garet,
    Thank you for the simple but powerful code.
    I have used it to implement an industrial login and it works great!
    Really appreciate your contribution.
blog comments powered by Disqus