MagTek Credit Card Reader in Linux

MagTek Credit Card Reader

I recently purchased a MagTek USB Swipe Reader to use as a credit card reader for a point-of-sale system I'm developing for my company. Since the POS system is written in Python and GTK+, and will be running on Ubuntu Linux, I needed to be able to interface with this credit card reader in Python and Ubuntu as well.

These are my adventures in getting the MagTek USB Swipe Reader to work in Ubuntu 10.10 (Maverick Meerkat). I hope this information helps others out there tinkering with credit card swipe readers in Linux.

To cut to the chase, I ended up writing this python program using PyUSB 1.0, the newer, development branch of the PyUSB library. I will discuss this code in detail later in this post. However, I was also able to inteface with the MagTek Swipe Reader using the built-in HID kernel module as well as the current, stable 0.x branch of PyUSB. But, they each had their flaws and ultimately PyUSB 1.0 stood out as being the simplest and sexiest approach for my purposes.

I AM NOT A USB EXPERT. This was one of my first projects involving USB directly instead of using one of those FTDI USB-to-serial devices. I just want to share a few nuggets of information that I learned along the way and then I will dig into my final test program.

Method 1: HID Kernel Module (hiddev)

When you plug the MagTek Swipe Reader into your USB port, the built-in kernel module for USB HID (Human Interface Device) will be loaded and the device is then available at /dev/usb/hiddev0 (or hiddev1, hiddev2, etc.). Therefore, the easiest way to interact with this device is to simply use open(), read(), and close() on that "file". This was the first method I got to work.

I was able to get this to work thanks to this blog post: Writing a magnetic card reader driver. While I wasn't trying to write a whole device driver, the use of the struct module to determine the read size and unpack the data was quite handy.

What I didn't like was blindly reading a filesystem device without knowing for sure that it was my intended vendorID and productID or being able to easily timeout from the read(). I also tinkered using the hiddev library in C, however, I really wanted to stay with Python and didn't want to get too complicated. But if you are interested in hiddev with python, take a look at Interrogating linux /dev/usb/hiddev0 in python.

Method 2: PyUSB 0.x

I learned quite a bit about USB while trying to get a read from the device using the version of pyUSB that was in the repositories for Ubuntu (python-usb version 0.4.2). I spent quite a bit of time with the USB specification and with the MagTek USB Swipe Reader reference manual. It was this time spent learning how the USB bus works and how the libusb library accesses devices that allowed me to fully appreciate the 1.0 branch of PyUSB (we're getting to that).

While I was able to get the data from the credit card and ensure it was the correct device using this library, it seemed a bit clunky for Python code. I read through Programming with PyUSB 1.0 and realized that's what I was looking for. Much more pythonic.

Method 3: PyUSB 1.0 ... Ah Ha!

Since PyUSB 1.0 is a development branch of the library, it is not available in the regular Ubuntu repositories and you will need to download and install it manually. You can download PyUSB 1.0 from SourceForge. Install instructions are in the README.

The modules used for the MagTek Swipe Reader were the usb.core and usb.util. You can pull up the docs on either of these like any other Python module using the pydoc command. For example:

pydoc usb.core

At the time of writing, I am using libusb 1.0 from the repositories, python 2.6.6 from the repositories, and PyUSB 1.0.0-a from the above mentioned link.

How the MagTek USB Swipe Reader Works

Before looking at the code, let's look at how this credit card reader works. You should download the USB HID Swipe Reader Technical Reference Manual from the MagTek website. As far as I know, the applicable MagTek USB Swipe Reader part numbers are 21040101, 21040102, 21040103, 21040104, 21040113, 21040114, 21040119, 21040139, and 21040143. You can find this part number on the bottom of the device.

The MagTek USB Swipe Reader is a relatively simple device as far as USB devices go. We actually do not even need to write to the device at all. We simply wait for the data from the swipe. We only have to worry about one configuration, one interface, and one endpoint.

When you swipe a card the MagTek credit card reader is going to send an "input report" for the data on the card out on the interrupt pipe. We can then read this binary data and break it up into it's elements according to the reference manual.

The Credit Card Reader Test Program

If you have not already worked with PyUSB 1.0, I highly reccomend you read the Programming with PyUSB 1.0 tutorial.

My full test program code is available here: magtek-pyusb.py. Let's take a closer look.

import sys
import usb.core
import usb.util

If the above import statements throw an exception, you probably do not have PyUSB 1.0 installed correctly. Again, this is the newer development branch and, at least at the time of writing, is not the version of PyUSB you will find in your repositories.

VENDOR_ID = 0x0801
PRODUCT_ID = 0x0002
DATA_SIZE = 337

This all came straight out of the reference manual for the device. We will use the VENDOR_ID and PRODUCT_ID to find our device. The DATA_SIZE is the size, in bytes, of the input report being sent to the interrupt pipe when a card is swiped. We use this to make sure we get the whole input report. If I get less than the 337 bytes then we request another swipe.

device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)

if device is None:
    sys.exit("Could not find MagTek USB HID Swipe Reader.")

Ah, look how easy that was! We didn't have to iterate the bus and the devices, we just used the very versatile find() method. We now have a usb.core.Device object for our USB Swipe Reader.

if device.is_kernel_driver_active(0):
    try:
        device.detach_kernel_driver(0)
    except usb.core.USBError as e:
        sys.exit("Could not detatch kernel driver: %s" % str(e))

If you recall, when you plug in the device, the built-in kernel module is likely to automatically load the HID driver for the reader. If that's the case then we want to detatch it so that we can interface with the device ourselves.

try:
    device.set_configuration()
    device.reset()
except usb.core.USBError as e:
    sys.exit("Could not set configuration: %s" % str(e))

The set_configuration() method called without parameters is going to use the first configuration found. Fantastic, our device only has the one. I am not entirely sure why the reset() method is necessary, however, I found that when I did not first reset the device the first input report would start at offset 7 instead of offset 0. In other words, the call to reset() ensures we get a complete read on the first swipe of a card.

endpoint = device[0][(0,0)][0]

Another demonstration of why I like PyUSB 1.0 over PyUSB 0.x. We can use the subscript operator to get random descriptors instead of having to iterate the configurations, interfaces, and endpoints. To read from the MagTek USB Swipe Reader, we need the interrupt endpoint which is the first and only endpoint. So the above line gets the first endpoint for the first interface and alt setting for the first configuration.

We now have the usb.core.Endpoint object we will need with the device's read() method.

data = []
swiped = False
print "Please swipe your card..."

while 1:
    try:
        data += device.read(endpoint.bEndpointAddress, endpoint.wMaxPacketSize)
        if not swiped: 
            print "Reading..."
        swiped = True

    except usb.core.USBError as e:
        if e.args == ('Operation timed out',) and swiped:
            if len(data) < DATA_SIZE:
                print "Bad swipe, try again. (%d bytes)" % len(data)
                print "Data: %s" % ''.join(map(chr, data))
                data = []
                swiped = False
                continue
            else:
                break   # we got it!

So here is where we read the swipe. We will be loading up a list called 'data' with the 337 bytes of data making up the input report. We use an infinate loop to poll the device waiting for a swipe.

Each time we call the read() method before a swipe has occured, it will raise a timeout exception. This is caught and since the 'swiped' variable will still be False, the loop continues. In otherwords, we continually timeout from the call to read() until a card is swiped.

When as a card is swiped, the read() method begins to return data instead of timing out and the 'swiped' variable is set to True. So now the 'data' list is added to with each iteration of the loop. Eventually all 337 bytes of data will have been read and the next call to read() will timeout again. This time, since the 'swiped' variable is now True, we assume the read is done. The size of the data read is checked against our READ_SIZE of 337 bytes. If the size of data matches then we break out of the infinite loop. Otherwise, if the data is too small, we assume that there was a bad swipe or some other problem. We reset the 'swiped' flag and the 'data' list and start the infinite loop over--effectively waiting for a new swipe.

enc_formats = ('ISO/ABA', 'AAMVA', 'CADL', 'Blank', 'Other', 'Undetermined', 'None')

print "Card Encoding Type: %s" % enc_formats[data[6]]

print "Track 1 Decode Status: %r" % bool(not data[0])
print "Track 1 Data Length: %d bytes" % data[3]
print "Track 1 Data: %s" % ''.join(map(chr, data[7:116]))

print "Track 2 Decode Status: %r" % bool(not data[1])
print "Track 2 Data Length: %d bytes" % data[4]
print "Track 2 Data: %s" % ''.join(map(chr, data[117:226]))

print "Track 3 Decode Status: %r" % bool(not data[2])
print "Track 3 Data Length: %d bytes" % data[5]
print "Track 3 Data: %s" % ''.join(map(chr, data[227:336]))

Once we have the binary data, we can chop it up according to page 15 of the reference manual. For this test application I'm just printing the info. The actual track data is converted to ascii when printed so that the data can be read as a string.

track = ''.join(map(chr, data[7:116]))
info = {}

i = track.find('^', 1)
info['account_number'] = track[2:i].strip()
j = track.find('/', i)
info['last_name'] = track[i+1:j].strip()
k = track.find('^', j)
info['first_name'] = track[j+1:k].strip()
info['exp_year'] = track[k+1:k+3]
info['exp_month'] = track[k+3:k+5]

And finally, since I'm reading credit cards, I want to parse out the bank data according to the 'B' format in track 1. I got this information from Magnetic Stripe Card > Financial Card on Wikipeida. Go figure.

In a final application, you would want to check the encoding type and the track 1 decode status before trying to parse out bank info since you will get data back even if there was not a good read. For example, you might get 'blank' as the encoding or a non-zero value for the track decode status on a bad read.

All in all, a pretty good start to using a MagTek USB Swipe Reader in Ubuntu Linux using Python. You will want to run this application as root so that you have permissions on the device. Here is a screenshot:

Credit Card Reader Output
Did you enjoy MagTek Credit Card Reader in Linux? If you would like to help support my work, A donation of a buck or two would be very much appreciated.
blog comments powered by Disqus
Linux Servers on the Cloud IN MINUTES