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
- 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:
Function | Panasonic pin/color | Omron pin/color |
Ground | 1/white | 1/green |
Ground 2 | 2/white | N/A |
+5 V | 3/white | 2/yellow |
DATA | 4/white | 5/brown |
CLK | 5/white | 4/red |
CARD_LD | 6/black | 3/orange |
- 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."
- 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.
- 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
frameat 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).
- 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.
- 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...
- 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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
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:
- Sketches and descriptions of the clock, data, and
card-loaded waveforms, with descriptions of their functions.
Include measurements of timing parameters of the clock rates on tracks
1 and 2.
- A sample track 2 manual reading from the scope (into ones and zeros) and a
decoding of this information into readable form (e.g.,
;000059873894042000?). Include a sketch of at least two
"words" in a row: both clock and data.
- Optionally, a sample track 1 manual reading and decoding.
- Example code (just one) for reading either track 1 or track 2 (or both,
if you made a combined flexible version).
- Successful output examples, masked for privacy.
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