Friday, November 27, 2009

Card Reader Tinkering

So, I have a habit of collecting electronics that even I can only refer to as "junk." It's even in a big box, labelled "junk." There's around 15kg of it that I just can't bear to leave behind every time I move... Needless to say, much of it consists of equipment pulls and junk I desoldered from old VCRs, microwaves, fridge relays, and such, and these are often sensors or actuators I want to try interfacing. This has turned into a regular hobby and has actually opened up a larger part of the world of electronics for me...



All that garble aside, I had a Mag-tek 21006505-based Track 2 magnetic card stripe reader sitting around, which I had purchased surplus from All Electronics way back in the day. It's a slide-through system that can be mounted on just about anything flat, and was purchased for ~$4USD... These days, they offer a different model for about the same price, but this one requires the card be inserted into a slot and removed quickly... Coincidentally, Hack Miami made a Sanguino interface program for this particular reader, but I didn't know about this until after my tinkering. Also, Sparkfun has a 3-track RS-232 serial reader, even an expensive USB reader/writer, but I can't say I'm "$60 interested" in this field. $4 for Track 2 is just fine with me.

So, why did I bother? Well, besides the thing just gathering dust in the Junk Box, who hasn't, at one point, wondered was was on the magnetic cards in their wallet? I had some hope that more interesting things than just the number on the front would be there, and I wasn't disappointed. I don't plan on using this anywhere else, so I just used the Arduino platform to get a quick prototype "sketch" going, reporting the information back through the Serial Port. Rather than just throw the code at you, however, I'll explain what's being read off Track 2, and what resources I used to figure this out...





Track 2 magnetic cards hold data in an alternating magnetic flux pattern in a strip similar to what is found in cassette tapes. Track 2 is usually used for numerical/banking data, dissimilar to Track 1 or 3 which often contain ASCII data, such as names or personal information. The Mag-tek21006505 reads an analog magnetic sensor, and returns data to a microcontroller at its own speed on three lines: /Data, /Strobe, and /Card Present. The "/" represents "not," which means that the data's logic value is inverted (a "1" is actually a "0", and vice-versa). I got most of what I know from a nice app note. /Card Present is supposed to go low when... Well, a card is present... But I found it a bit unreliable, and just stuck to reading /Data when /Strobe becomes active, which indicates that the /Data line has a valid data bit. The smart way of doing this is by using an interrupt, but because I was just tinkering and not planning to integrate the code into a system, I just used a polling-based system that wastes cycles waiting for the next /Strobe pulse.




The reader uses a RJ-25 plug, which I almost immediately cut off for lack of an interface board. I soldered in a simple right angle header instead, for the inevitable breadboarding this reader would experience. /Data is tied to Digital 3 on the Arduino, /Strobe to Digital 2. /Card Present is ignored, and I tied both the shield and ground cables together to the Arduino ground reference. The power line went directly to the Arduino 5VUSB. The following are the colours of the wires inside versus their function, though you'll probably want to crack it open and verify on the board:

1 - Brown - /Data
2 - White - /Card Present
3 - green - /Strobe
4 - Black - GND
5 - Red - +5V
6 - None - Cable Shield (should be pretty obvious)

So, Track 2 has a whole lot of flux reversals before the actual data, interpreted as a whole load of "0" bits from the Mag-tek chip before the real data comes through. Thankfully, the "Start Sentinel" character that precedes any data has a whole lot of "1's," and is easy to identify, and use as a starting point to start counting bits into byte data. Track 2 uses 4 bits with 1 odd parity bit to represent base 10 numbers and a few control characters (field separators and the like). Thanks to an extremely helpful writeup from Acidus, I was able to translate the codes. Unfortunately, I didn't bother with parity checks, and didn't implement all control characters, since it would be a waste to just present the characters without knowing what they mean anyways. Also, I don't use the LRC, which is another weakness, but I have yet to confirm any incorrect information. The code simply reads in the data, throwing out the parity bit, processes it, then displays it on the COM port.

So here's where I throw the code at you:

/* This sketch reads from a Mag-tek 21006505 Card Reader, which looks at track 2. It doesn't check the
 parity bit, and is hard-coded to read up to 76 characters. This length is arbitrary and can be
 changed by the user, but what is output is processed for field separators and end stops, which
 in tests seemed to be a "?>", or a stop sentinel followed by a control signal (see below). 
 First it takes in data, with timeouts if the stream stops early, then it processes it and
 writes it to the serial port at 9600 baud.

  Here we present a table of the data bits received versus their char versus their meaning, taken
  from the article "Magstripe Interfacing - A Lost Art" by Acidus (avidus@yak.net):
  
  b0 b1 b2 b3 Char
  0  0  0  0  0
  1  0  0  0  1
  0  1  0  0  2
        .
        .
        .
  0  0  0  1  8
  1  0  0  1  9
  0  1  0  1  :  (Control)
  1  1  0  1  ;  (Start Sentinel)
  0  0  1  1  <  (Control)
  1  0  1  1  -  (Field Seperator)
  0  1  1  1  >  (Control)
  1  1  1  1  ?  (End Sentinel)
  
  These correspond to to binary values, of course, and are treated as decimal values in the code.
  Also, please bear in mind /Strobe and /Data are, in fact, inverted logic, so a "0" is effectively
  a "1," and is treated as such.
  

*/

#define notstrobe 2
#define notdata 3

char data[80];
int i, imax, j, k, kmax, timeout, datastart[15], dataend[15];


void setup() 
{ 
  Serial.begin(9600); 
  pinMode(notstrobe, INPUT);
  pinMode(notdata, INPUT);
  digitalWrite(notdata, HIGH); //Activate Pullups
    digitalWrite(notstrobe, HIGH);
  // prints title with ending line break 
  Serial.println("Mag-tek 21006505 Card Reader Interface"); 
} 

void loop() 
{ 
  data[0] = 0;
  if (!digitalRead(notstrobe)) // If we see the strobe line come down, the data stream starts.
  {
    while(digitalRead(notdata)) {}; //Wait until leading 0's are gone.

  for (j=0;j<76;j++) // Collect 76 characters, just in case
  {
    data[j] = 0;
  for (i=0;i<4;i++) // Take in 4 bits!
  {
   data[j] |= !digitalRead(notdata)<<i; //Read data while strobe is low
    // Serial.print(!digitalRead(notdata));
    timeout = 0;
    while(!digitalRead(notstrobe) && timeout<500) {timeout++;} // Let strobe go high...
    timeout = 0;
    while(digitalRead(notstrobe) && timeout<500) {timeout++;} // ...then low again before looping back
  }
    timeout = 0; // Ditch the parity bit for now by waiting another strobe.
    while(!digitalRead(notstrobe) && timeout<500) {timeout++;}
    timeout = 0;
    while(digitalRead(notstrobe) && timeout<500) {timeout++;}
   
  }
  i = 0;
  k = 0;
  for(j=0;j<76;j++) // Print out the stream
  {
    if (data[j] == 11)
    {
      Serial.print("Start Sentinel!\n"); // This announces the start of the datastream
    }
    else if (data[j] == 13) Serial.print("-"); //This is a field separator, so we'll use a hyphen
    else if (data[j] == 15) Serial.print("?"); //This is an End Sentinel
    else if (data[j] == 14) Serial.print(">"); //This is a 
    else if (data[j-1] == 14 && data[j-2] == 15)
    {
      Serial.print("\nEnd of Card!\n"); //This is supposed to announce the end of the datastream
      break;
    }
    else if (data[j] > 10) // These are not numbers, and can be debugged at the user's leisure
    {
      Serial.print("\n Not a Number!");
      Serial.print(data[j], DEC);
      Serial.println();
    }
    else Serial.print(data[j], DEC); //If it's actually a number, just print it! It's data!
  }
 }
 
}

The code worked quite reliably, and I enjoyed pulling my wallet apart for the following minutes. Credit cards are pretty boring, as I expected, containing only confirmation codes after the codes written on the card (which are probably used by the bank to check remote transaction requests), but my BC Driver's License had some really interesting codes appended to it, besides the DL#, expiry, and my birthday, some coinciding possibly with my own personal data, like weight and height. I imagine Track 3 is even more interesting. Most membership cards (i.e. a Roger's Video card) are exactly what is written on the back, exactly the same as the bar code sometimes also found there. Translink Faresavers are pretty interesting, though I'm not sure what is junk and what is actual track data, given the complete mismatch of Track 2 data and what is written on the card itself. I'd like to check out some Translink U-Passes when I get the chance.

If anybody else comes up with some interesting data, please post it here!

Some obvious improvements to the current code would include parity checks, LRC checks, and full definition of control codes for those more familiar with them. Arguably less obvious improvements would include making a reader an object with mounted interrupt functions for when /Strobe goes low, to avoid cycles, but to store data that can be accessed when desired... Or perhaps a polling function with an automatic time out... Regardless, some kind of function definition, as opposed to everything vomited into "void loop()."

If anybody really wants, I can probably make this a library, but I probably don't want to touch interrupts if that's the case, otherwise inexperienced users might get confused when their code has strange microsecond timing errors due to the hardware interrupt.

Well, I hope somebody finds this code useful!

Post Script: I apologize for the low quality of this post; I had written a much nicer instruction set prior, but Blogger decided to delete it for some reason during editting when I pressed "ctrl-z", then immediately autosaved and decided to forget its undo history, blocking any further recovery of the post. It was amazing. I now write up and save my posts in Gedit before putting them anywhere near Blogger. Just one more reason I don't trust this whole move to online applications/Operating Systems.

No comments:

Post a Comment