Lab 8: Magnetic Card Reader: What's in Your Wallet?

In this lab, we will use a packaged magnetic strip reader, which has the on-board smarts to convert a sequence of magnetic fluxes into a data stream with associated clock. At first, we will look at this data directly and decipher the contents of a magnetic stripe.

Then, we will form an interface to the Raspberry Pi's GPIO, using interrupt capability to catch pulse edges and act accordingly. The goal of the Python program(s) will be to read the raw data, interpret the characters contained within, and present the results (with parity assessment) to the screen.

The Actual Lab

Some of the instructions below are less than fully descriptive. You are left to figure some things out on your own. Scan the Tips before starting to have a sense of what is available to save time as you go.

Lab Procedure

  1. Hook up the card reader to a breadboard, providing it with 5 V from the bench supply (Pi not involved yet) and identifying the leads that correspond to card-loaded, data, and clock. Note that the mounted card readers (Panasonic) are built to read track 2. A simple zip-tie shim with 1/8 inch spacing (helps to kink it several places so it holds its position better) will let you read track 1 with these readers. We also have a few fully-packaged readers (Omron) that have been modified to read track 3. One or two shims allow these to read tracks 2 and 1, respectively. Note the pinout is different for the two, and go as follows:

    FunctionPanasonic pin/colorOmron pin/color
    Ground1/white1/green
    Ground 22/whiteN/A
    +5 V3/white2/yellow
    DATA4/white5/brown
    CLK5/white4/red
    CARD_LD6/black3/orange

  2. Use an oscilloscope to probe the data, clock, and card-loaded lines all at the same time. Trigger on the falling edge of card-loaded, and swipe a card, adjusting the timescale to get the full action. Sketch the 3 waveforms (crudely) for the write-up. See the tip below about zooming in on the action. Be sure you understand the roles and relative timings of all three data streams. Describe why all three signals might be called "active-low."
  3. Swipe the card with the zip-tie out of the reader (no shim), which will read track 2, and characterize the clock period. Then do the same for track 1, with the 1/8-inch zip-tie shim in place. For roughly the same swipe speed, is there a difference in clock/data rate? What do you infer about the density of data on the card for these two tracks? Note: your student ID doesn't have track 1 data (try it!), but credit cards or drivers' licenses do.
  4. Now just look at data and clock, trigger on the falling edge of the data channel, and set up the scope triggering for a single sequence (capture). Swipe track 2 of a card (no shim), adjusting the scope time-base to get the (fast-ish) swipe all on the same frame—at least all the data transitions. Swipe the card so that as you look at the magnetic stripe across the bottom of the card, the reader reads left to right across the stripe (or if holding the card so the stripe appears across the top—which probably puts any printing on the card upright—the read direction would be from right to left).
  5. Pore over the data record, jotting down all the ones (low) and zeros (high) corresponding to each clock pulse. See tips below about zooming and tracking oscilloscope data. It's a huge string, but worthwhile. The bits should clump into groups of five, having a guaranteed odd number of ones within each group (a nice way to error check and keep the bit divisions straight as you go). You may find it helpful to use the scope's vertical cursor bars to delimit packets of five (see tip below). Once you have the data, look at the 5-bit code detailed in the magstripe documentation, realizing that the least significant bit is first, and the parity bit at the end. Since track 2 is mostly numbers, you should ultimately be able to read the data directly off the scope: 11100 is a seven, for instance. If you use your student ID, you'll see lots of zeros (00001). Include a sketch of at least two interesting (non-zero) packets in a row, both clock and data.
  6. You may optionally want to go through the same exercise with track 1, which has denser data (thus more of it to wade through), and follows the 7-bit code. But it is gratifying to read your name off the oscilloscope one character at a time...
  7. Now it's time to have the Raspberry Pi do the job. But it is first important to realize that the Pi's GPIO is based on 3.3 V logic, and the 5 V inputs could damage the Pi. The card readers operate on 5 V, and put out 5 V logic levels. So we're stuck, right? Well, a simple voltage divider using a 2:1 ratio between resistors (in the few kΩ range) will knock 5.0 V down to 3.3 V. So before connecting to the Pi, arrange voltage dividers for each of the three signal lines: feeding the "top" of the divider with the card reader line and tapping the divider junction to get 3.3 V logic.
  8. Pick three pins on the GPIO header to use for CARD_LD, CLK, and DATA. It does not matter which ones; just note the BCM numbers. Also pick a GPIO for a monitor signal that will help diagnose and troubleshoot. Set up the breadboard so the Pi supplies +5 V and ground (no longer need the bench power supply), and connects the card reader to the Pi pins (through dividers), also permitting you to probe the relevant lines on the scope.
  9. Start a Python program, importing sys, time, and RPi.GPIO (optionally as GPIO). Use the knowledge you gained in the DAC-GPIO lab to inform your setup. Define variables (like CLK) to map to your pin choices, and set up the three from the card readers as inputs (GPIO.IN), and the monitor pin as output. We will be using the card-loaded and clock pins as interrupt sources, so initiate these two with something like

    GPIO.setup(PIN_NAME, GPIO.IN, pull_up_down-GPIO.PUD_UP)

    where PIN_NAME is a stand-in for your defined pin name. The purpose is to make sure these lines sit high even if not asserted by the card reader to prevent accidental interrupt triggering. Initialize the monitor pin to output zero (GPIO.LOW). To make things easier later (for switching between track 1 and track 2), it is helpful to define the bit period, start sentinel, and character map (in accordance with the documentation). For instance, for track 2, we might define:

    per = 5
    charmap = "0123456789:;<=>?"


    But feel free to rename variables as you see fit. Oh, and one last detail before moving on. Include the line:

    GPIO.wait_for_edge(CLOCK_VAR_NAME, GPIO.FALLING, timeout=10)

    prior to the start of the capture sequence (below). Substitute your actual variable name for the clock. It seems the first instance of setting up an interrupt for a pin takes 30–40 milliseconds, and this can impact the speed with which the capture sequence is ready to start, forcing low swipe speeds. By putting this throw-away line at the top (which gives up and moves on after a 10 ms timeout), the function will be fast/prompt when we need it to be once the card is actually swiped, so speeds can be higher.
  10. The rest of the effort is basically divided into two pieces: capture, and interpret. It is possible to interleave these functions for a more "real-time" feel to what's going on, but for simplicity we will treat them separately, in sequence. So for the capture piece, a bare-bones version looks like this:

    loaded = False
    seq = []              # list to hold sequence of ones and zeros
    grace_ms = XXXX       # decide how many milliseconds to allow
    print "Swipe Card: you have %d seconds" % (grace_ms/1000.0)
    beg = time.time()                            # grab a time in sec.
    GPIO.wait_for_edge(XX, GPIO.FALLING, timeout=grace_ms)  # card load edge
    now = time.time()
    dt = now - beg
    if (dt > grace_ms/1000.0 - 0.1):             # within 0.1 s of timeout
      print "Timed out. Cleaning up and exiting."
      GPIO.cleanup()
      sys.exit()
    else:
      print "Card Load detected"
      loaded = True
    beg = time.time()
    now = beg
    while ((now - beg) < XX and loaded):         # give it some time
      GPIO.wait_for_edge(CLOCK_VAR_NAME,GPIO.FALLING,timeout=100)
      bit = 1 - GPIO.input(DATA_VAR_NAME)        # active low
      seq.append(bit)
      if GPIO.input(CARD_LOADED_VAR_NAME):       # replace name
        loaded = False
      now = time.time()

    You'll need to replace stand-in variable names and decide time allowances (replacing XX instances). Also, you are encouraged to customize your variable names so you are more likely to actually understand what this code is doing. It's risky just providing this code skeleton: you may just grab and use without trying to learn. But without it, the lab may grind to a halt.
  11. Print the sequence list, and look it over. This should produce something essentially identical to your manual exercise recording ones and zeros. Scan through it: do you see the start sentinel, and can you make sense of the data blocks? If not, make sure you are swiping in the correct direction. Until this section works properly, no point in continuing. If you need diagnostics, it can be helpful to create monitor pulses at points within the program, by putting in things like:

    GPIO.output(MONITOR_PIN_NAME,GPIO.HIGH)
    time.sleep(0.0001)         # 0.1 ms
    GPIO.output(MONITOR_PIN_NAME,GPIO.LOW)

    These can be put anywhere to create a pulse (within any loop) to study the timing and make sure the system is reacting properly to signal edges (interrupt/wait functions).
  12. Now for the decoding. The idea is to look for the first "one" data bit and start counting in 5 or 7 chunks (according to per variable), interpreting the character and parity along the way. Here we go.

    msg = ''                      # will hold message content
    par = ''                      # will hold parity indicators
    pen = 0                       # penalty count
    lrc = 0                       # long. redund. check
    first = seq.index(1)                # finds first one in seq
    n_char = (len(seq) - first)/per     # integer blocks after first
    not_end = True
    for ind in range(n_char):
      parcel = seq[first+per*ind:first+per*(ind+1)]   # slice out one block
      n_ones = sum(parcel)
      if n_ones % 2 == 0:         # even number of ones (bad)
        par += 'X'                # indicate bad
        if not_end:               # still in valid sequence
          pen += 1                # add to penalty
      else:
        par += '.'                # indicate good
      strn = ''
      for val in parcel:
        strn += "%d" % val        # append 1 or 0
      map_ind = int(strn[:per-1][::-1],2)
      msg += charmap[map_ind]
      if not_end:
        lrc ^= map_ind            # accumulate LRC for valid data
      if (msg[-1] == '?' and ind > XX):         # end sentinel for all tracks; after so many
        not_end = False           # reached end of legit
    print "%s  LRC = %s" % (msg,charmap[lrc])
    print "%s; penalty = %d" % (par,pen)

    Make sure you understand how the parcel is formed as a slice. For this and similar things, use an interactive Python window to explore the syntax and verify how things like indexing should work. The funkiest bit of this code is the formation of the message out of charmap. The thrust is to build a string of ones and zeros and then convert to an integer as an index to the character map. We need to ignore the parity bit (last one; thus the [:per-1] slice), then have to reverse the bit order (thus the [::-1] construction), before passing to int(string_val,2) to convert to an integer from binary (base 2). For the start sentinel, strn looks like '11010'; the first slice operation makes it '1101'; the reversal makes it '1011', and int('1011',2) returns 11. Finally, charmap[11] returns ';' for the track 2 definition.

    If you have a valid sequence, this code (after some slight adaptation/modification) should produce messages and parity indications consistent with what you would do manually. The longitudinal redundancy check (LRC) is a way to check the health of the overall message. Most cards have a character after the end sentinel that should be the bit-wise XOR combination of all characters in the message, including the sentinels. The code keeps track of this XOR operation and presents its final answer for comparison to what's in the data stream. When these match up and the parity penalty is zero, it's almost certainly a perfect read/capture. Once things are working well, you might decide to eliminate any verbose printing you have included, and only print the "good" stuff. If you need to debug, one recommendation is to build a string out of all the individual strn segments with a space between each one and print this out so you see if it's getting the right characters, grouping them correctly, etc.
  13. After working reliably on track 2 data, copy the program and modify it to support track 1. Insert a shim in the reader and see if you can decode correctly. See the tip about the character map string.
  14. Once it all works, run through at least three track-2 cards and record the information listed above. See the tip below about maintaining privacy and guarding sensitive information. Identify the segments that you can decipher as making some sense: student ID, account number, expiration date, etc. Does the LRC match the final character after the end sentinel? If you get any even numbers in the parity string (marked by an X and a non-zero penalty), swipe again: there was a mis-read.
  15. Run at least two track 1 cards using 7-bit mode. If the parity is all odd (penalty = 0), the read was good. Include samples of the information, again writing the output string and parity right below, as well as LRC check. Identify fields and mask sensitive information.
  16. If you are motivated to do so, use one of the Omron readers (beware: pinout is different; see table above) and read a track 3. You may have to hunt around to find a card that actually uses track 3. It should be in 7-bit mode.
  17. Have fun figuring out what's in your wallet! Include in the writeup the thing you found most surprising or interesting about what you saw.

Tips

  • It is most useful to have the scope in normal trigger mode (rather than auto: see MENU button in Trigger section on scope). You may even want to arm for a single acquisition by hitting SINGLE each time before you swipe (Run/Stop cycles back to normal).
  • To zoom in on the data stream, use the little magnifying glass button n the wave-inspector area. An overview of the selected channel should appear in the top portion indicating the zoom area, while the zoomed version is on the main display. The nested knobs should allow panning and zooming to inspect the details of the waveforms.
  • To use cursors, hit the Cursors button, and select vertical (time) type. Now the Multipurpose knobs control the positions of the two cursors (also note use of Select). It is very handy to set the cursor separation to be 5 (or 7) clock periods apart, so that you can scroll the data sequence through and frame one word at a time. Be careful, though, since a non-uniform swipe speed will make this period change as the swipe goes along.
  • The code on the html page was transcribed and modified from running code, but may have small errors that prevent proper operation. But it's better than being told you have to figure it all out on your own (like you would in the real world), right?
  • Put a line: GPIO.cleanup() at the end of the program to reset the GPIO pins you used in the program. It's good etiquette, and may prevent some warnings.
  • For track 1, It may help you to use the following string as a character map.

    " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"

    Note that some characters, like the double-quote and the backslash must be "escaped" with a backslash. It is a good idea to verify proper indexing of the string, either by a test line in the code or via an interactive session. For instance, charmap[2] should produce the double quote; charmap[3] should be #, charmap[20] is the number 4, and charmap[61] is ].
  • If you are having trouble getting reliable reads, don't understimate the role of the shim. Try different ones (different widths) to find the sweet spot.
  • If you are so inclined, you can make a single program that accepts a command-line argument to indicate whether you are reading track 1/3 or track 2 data and have the code behave correctly either way.
  • If you are so motivated, it is possible to rig your program to try interpreting the sequence of ones and zeros in either direction, so that swipe direction is not critical. To do this, you would add another decode block after executing seq.reverse(). The processing then looks identical, and you can even decide in the end which of the two decodes you are going to print out based on the penalty score.
  • Rather than write potentially sensitive information in the write-up, you may substitute Xs for random characters so-as to render your information private/unusable by others. Try to keep innocuous things (like expiration date or name) intact so that it is easier to judge whether it works or not.

    Lab 8 Write-up

    The due-date of the write-up for this lab will be at the time of the exam on Dec. 12.

    Specific items to include are:

    For any who are interested, this was a two-part lab in the past, first building a relatively ambitious digital circuit on a breadboard that packaged the data streams into 5- or 7-bit chunks and exported RS-232 serial that could be read on the labs Windows machines (blech!). A PIC microcontroller was part of the design, whose programming gathered the data chunks every 5th or 7th bit and prepared an asynchronous package for export as RS-232.


    Back to Physics 122 page