Sunday, April 13, 2014

GPS Lockbox "Guts"

Often with complex projects which involve a lot of different components which have to talk to each other, I will work on the guts in stages without worrying about what the final product will look like.  Using getting the guts to work together is a huge part of the project and the cosmetic details are a secondary consideration.  If the "insides" don't work, it doesn't matter how pretty the outsides look.

This project is envisioned to be a modification of the lockbox which allows the box to open when a certain location is reached (rather than when a certain code is entered).  The lockbox then is a little like the inverse of going geo-caching.  Rather than knowing the GPS coordinates but not having the item at those coordinates, you have the item but not the coordinates.

The guts of this project are shown in the picture at left.  The GPS modem (MediaTek 3328, labelled GTPA010, datasheet here) returns time, latitude, longitude, and altitude at regular intervals (2-3 seconds or so) using a serial connection.  The LCD is the same one I used for the calculator, although I made a nice PCB board for it to simplify the connection process.  Eagle CAD files for the board are here.  This is necessary because the LCD board requires 8-bit mode and therefore has 16 pins and requires 10 inputs plus +5 and GND.   The board even has a spot for a 10k potentiometer so you can adjust the contrast of the LCD.  The keyboard is the same one used for the lockbox and the calculator, but this time I've routed it through a I2C GPIO board from dfrobotics.   The RGB LEDs will be used to provide the user with visual feedback of some type depending on whether he/she is getting closer or farther away from the destination target.  Think of them as a visual way of playing "hot or cold".  The servo will open the lockbox.  Estimated costs:  $25 for Arduino Micro + $15 for Keypad + $6 for LCD + $13 for I2C GPIO + $12 for servo + $40 for GPS =~ $80 + consumables.

The current setup uses every single pin of the Arduino Micro.  Check out the pins assignments:

I started with getting the GPS modem working.  There was lots of great help online for doing this, and I want to credit these two websites for getting me all set: http://letsmakerobots.com/node/34050 and http://forum.arduino.cc/index.php?topic=7490.0.  The first showed me how to hookup the GPS using a softserial interface.  Apparently for this modem a normal hardware serial interface doesn't always work, but the software serial port seemed to do the trick.  It also took me a while to realize or find out that SoftSerial only works on certain pins... this problem was a recurrent one as I switched around pins or migrated to a different Arduino board.  For my original setup, I was using a 1280 Mega, and found out that the "Not all pins on the Mega and Mega 2560 support change interrupts, so only the following can be used for RX: 10, 11, 12, 13, 14, 15, 50, 51, 52, 53, A8 (62), A9 (63), A10 (64), A11 (65), A12 (66), A13 (67), A14 (68), A15 (69)" - Arduino SoftSerial Documentation.  Once I used the appropriate pins, and a simple program to setup the SoftSerial connection and then read in the available data, I was seeing a stream of text from the GPS.

#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3, false);
void setup()  
{Serial.begin(4800);
  mySerial.begin(4800);}
void loop()
{if (mySerial.available())
    Serial.write(mySerial.read());
} 




The next step was to take the serial input data and parse it into useful information.  The MediaTek datasheet shows that the GPS outputs data in several modes (GGA, GSA, GSV, RMC, VTG) and I was seeing parts of all of those modes.  However, the mode that I was consistently getting a full string from was GGA.  I could tell because I could see the string starting with $GPGGA and ending with *OF or some similar two hexadecimal checksum starting with '*'.  So I decided to parse that string.  My savior was codename GeckoCH from the second site listed above, who posted code to parse a GPGMC type string.  I was able to adapt this code to read in serial data until it found "$GPGGA", then pull the time, latitude, longitude, and altitude.  I have not implemented a checksum check because the datasheet does not show how the checksum is calculated.

The function readline pulls in serial data until a buffer overflow would occur (bad) or a carriage return is seen (good).  According to the GPS datasheet, a carriage return is the end of each sequence.

void readline(void) {
 char c;
  bufferIndex = 0;
  while (1) {
      c=mySerial.read();    // read a byte
      if (c == -1)          // if the byte is -1 don't record and skip to the next byte
        continue;
      //Serial.print(c);
      if (c == '\n')        // if the byte is newline don't record and skip to the next byte
        continue;
      if ((bufferIndex == BUFFERSIZE-1) || (c == '\r')) {    // if buffer overflow OR byte is a carriage return, reset the buffer and quit readline
        buffer[bufferIndex] = 0;
        return;
      }
      buffer[bufferIndex++]= c;  // otherwise store the byte
  }

The only problem with this function is that if the GPS is sending nothing, this function doesn't break out of the infinite loop.  That's a bug to be fixed later.  Next in the main loop we look for the first six characters of the buffer to match the string $GPGGA.
if (strncmp(buffer, "$GPGGA",6) == 0)  // Parse the string when it begins with $GPGGA
   { 
    Serial.println(buffer);

This is somewhat confusing since when we look at the buffer, the first six characters are never $GPGGA; they are other parts of the string from the modem.  But I think somehow the buffer gets shifted to the left each cycle so that at some point the string $GPGGA ends up at the front.   Anyhow, it works.  When that happens (and only when that happens), we parse the string looking for deliminators like decimals and commas, and pull out the appropriate data between those deliminators.  For example, the code below finds the longitude by looking after a comma.  It skips the decimal (because floating point math is notoriously bad) then adds in the numbers after the decimal.

// longitude
    parseptr = strchr(parseptr, ',')+1;
    longitude = parsedecimal(parseptr);
    if (longitude != 0) {
      longitude *= 10000;
      parseptr = strchr(parseptr, '.')+1;
      longitude += parsedecimal(parseptr);

uint32_t parsedecimal(char *str) {
  uint32_t d = 0;
  while (str[0] != 0) {
   if ((str[0] > '9') || (str[0] < '0'))
     return d;
   d *= 10;
   d += str[0] - '0';
   str++;
  }
  return d;
}   

GPS data comes in at least three formats that I have seen.  The first is a straight decimal.  For example, the location of the school that I work at is latitude: 47.6129121, longitude: -122.3161762 (I found this using this cool website for which you can enter an address and find the GPS coordinates).  Latitude is a degree between -90 and 90, where 0 is the equator, the northern hemisphere is positive numbers up the to north pole (90), and the southern hemisphere is negative numbers down to the south pole (-90).  Longitude is measured relative to the prime meridian and goes from -180 to 180 with negative numbers for longitudes west of the prime meridian and positive numbers east of it.

The second and third formats are Degrees, Minutes, Seconds and Degrees, Minutes.  In the latter format, my school's address is N47° 36.7747', W122° 18.9706' .  For more details on going back and forth between these types, see http://en.wikipedia.org/wiki/Geographic_coordinate_conversion.   According to the Mediatek datasheet, the GPS output is in the format ddmm.mmmm for latitude and dddmm.mmmm for longitude.  This means the first two/three numbers are degrees, and the last 6 are minutes with a decimal.  The raw output at my school is shown below, which we can interpret to mean N47° 36.7557', W122° 18.929' . This is pretty close, although not perfectly aligned with the online coordinates.

It will be useful for me to convert from the GPS output (eg ddmm.mmmm) to a decimal because I will be wanting to calculate the difference between the current location and the destination location.  This conversion can be done (using floating point math for now...may need to change is precision suffers) by using the equation DD = D + M/60.  So, for example, if ddmm.mmmm = 4736.7557, then DD = 47 + 36.7557/60 = 47.6125. More on that in a subsequent post.

The test code I downloaded (from codename GeckoCH) prints out latitude and longitude in Degrees, Minutes, Seconds format using the integer and mod trickery below (note that previously the raw value from the GPS modem was multiplied by 10,000 to eliminate the decimal.

      Serial.print(latitude/1000000, DEC); Serial.print("\260"); Serial.print(' ');
      Serial.print((latitude/10000)%100, DEC); Serial.print('\''); Serial.print(' ');
      Serial.print((latitude%10000)*6/1000, DEC); Serial.print('.');
      Serial.print(((latitude%10000)*6/10)%100, DEC); Serial.println('\"');

The output from the computer's serial monitor looks like this:


I would like the final lockbox to be a stand-along unit which doesn't require a computer.  Therefore, I needed to add a keypad to put in data, and an LCD to display data.  The LCD gave me serious trouble at first; displaying letters only in uppercase or lowercase, and displaying gibberish when I tried to print certain characters (such as a space or !) to the screen.  I had some luck troubleshooting this behavior using test code found from this website:  http://www.microcontroller-project.com/displaying-ascii-characters-on-16x2-lcd-with-ardunio.html which helped me to see that the LCD was displaying only the numbers 0-9, the letters A B C D E, the remainder of the alphabet in lowercase, < > = .  Using the code below, it cycled through this sequence several times before cycling through a set of what looked like chinese characters.

void loop()  {
  int count=33;
  char ascii=0x00+33;
  while(count!=235) {
    lcd.setCursor(0, 0);
    lcd.print("DECIMAL = ");
    lcd.print(count);
    lcd.setCursor(0 , 1);
    lcd.print("ASCII = ");
    lcd.print(ascii);
    count++;
    ascii++;
    delay(1000);
    lcd.clear();  }
}


When I migrated from the Arduino Mega to the Arduino Micro and used a different LCD with my previously mentioned PCB board, the problem disappeared.  I haven't tested the original LCD to see if the problem was with it or the wiring to the Mega.

The keypad is being read from using a GPIO board.  I initially tried the wiring and code displayed on their website: http://www.dfrobot.com/wiki/index.php/Arduino_I2C_to_GPIO_Module_%28SKU:DFR0013%29 which failed to produce output which varied at all.  In fact the sample code just returned 69 (ascii for 'E') regardless of what button was pressed.    In looking at the code, you can get a sense of what it does.  The function below first toggles one of the output pins; the outer for loop does this, setting 0.7 (10000000) high and the other three outputs low, then bit shifting (input = input >> 1) so that 0.6 (01000000) is high, and so on.  While one of the columns is high, it then scans the inputs looking for 1000 (0x8), then 0100, 0010, and 0001.  If it finds a match, it records the values of i and j (which correspond to the column and row of the key pressed) and then calls another function (outputchar) which just returns the key pressed.

unsigned char readdata(void)          //main read function
{
  int input=128;                      //binary 10000000, set pin 0.7 HIGH
  for (int i=0;i<4;i++)               //for loop
  {
    write_io (OUT_P0, input);      
    unsigned int temp=0x8;            //temporary integer, binary 1000, to compare with gpio_read() function  
    for (int j=0;j<4;j++)
      {  
         if (gpio_read(PCA9555)==temp)
         { return outputchar(i,j);}   // output the char
         temp=temp>>1 ;               // shift right
      }
    input=input>>1;                   //shift right, set the next pin HIGH, set previous one LOW
  }
  return 'E';                         // if no button is pressed, return E
}

I tried several troubleshooting techniques.  The first was to use a 5V supply to apply voltage to each of the columns, then press and read the rows using a multimeter.  Sure enough, only when the correct button was pressed did I see 5V.  This told me that the keypad was working fine.   Second, I tried to apply 5V directly to one of the inputs.  I reasoned that this should at least trigger an output other than 'E'.  It did not.    Next I simplified the code, just having it print the value of the 4 input pins (eg print the value of gpio_read(PCA9555)).  I noticed that the output was always 15 (decimal for b1111), which meant that the inputs were pulled high.  This suggested there were pullup resistors on the inputs.

 See http://www.francisshanahan.com/index.php/2009/what-are-pull-up-and-pull-down-resistors/ from which the picture at left is taken.  With pullup resistors, you want to ground the input and look for a low signal.  In the picture at left, when the switch is closed, the input sees GND (digital 0).  With this in mind, I modified the readdata function to look for one of the inputs to be LOW (eg 0111 or 1011 or 1101 or 1110).  One way to code this is to use the code below:

unsigned int temp=0x0F;   //binary 1111
temp ^= (1 << j);  

The ^= symbol is an XOR logic, or exclusive OR.  It compares the two inputs, and if ONLY ONE of the inputs is a 1, it returns a 1.  So if both of the inputs are 1 or both of the inputs are 0, it returns a 0.  In this case, we XOR 1111 with 0001 (when j = 0).  This returns 1110, since the first three bits were a 1 XORed with 0 (giving 1), but the last bit was a 1 XORed with a 1 (giving 0).  Basically, this a tricky way to turn OFF the bit in the "jth" position.   With this code substituted into the inner for loop, I finally saw changes in the output from 15 (1111) to 14 (1110) or 13 (1101) or 11 (1011) or 7 (0111) when buttons were pressed.  However, it didn't matter which row was being pressed.  This suggested that something was still wrong, and that the problem was with how I was setting the outputs.

I guessed that maybe I should be setting only one of the outputs LOW, instead of setting only one of the outputs HIGH, and low and behold, it suddenly all worked.  A wonderful moment.  In retrospect, this is obvious.  Instead of setting one line HIGH and then reading all the outputs for the line looking for one to be HIGH, if the default is already HIGH, we should set one line LOW then read all the outputs looking for one to be LOW.  The updated code which works is shown below:

unsigned char readdata(void)          //main read function
{
  for (int i=0;i<4;i++)               //for loop
  {
    int input=0xF0; 
    input ^= (1 << (i+4));            // toggle output LOW (outputs are 5th - 8th bits)
    write_io (OUT_P0, input);    
    for (int j=0;j<4;j++)
      {  
         unsigned int temp=0x0F;   //binary 1111
         temp ^= (1 << j);  
         if (gpio_read(PCA9555)==temp)
         { return outputchar(i,j);}   // output the char
       }
   }
  return 'E';                         // if no button is pressed, return E
}

The last piece of the hardware puzzle was the RGB LEDs.  I found this very helpful instructable:  http://www.instructables.com/id/RGB-LED-Tutorial-using-an-Arduino-RGBL/ and basically worked off of that.  One test of the analog code (which fades between colors) sold me on the fact that using digital outputs is not very cool.  This is because the digital outputs only turn ON or OFF each of the Red, Green, and Blue LEDs, allowing only certain colors.  If you use a PWM output for each of the colors, you can vary the intensity of each color, allowing basically all the possibly colors.  So much cooler.  Not clear on what I mean?   Watch these two videos and tell me which is cooler :)

https://www.youtube.com/watch?v=TwoXCqJAtLw  (Digital)
https://www.youtube.com/watch?v=SbdazwSjIOU (Analog)

Here's a video of my actual RGB LEDs:

The goal here will be to have the rate of fading depend on how close the user is to the GPS target... speeding up as he/she gets closer.  I've got a little work to do, since the fading code from the instructable is synchronous (eg it uses delay functions to dictate the speed of the code).  I need the LEDs to change colors asynchronously so that the communicates with the GPS can continue to happen.  This shouldn't be too hard by using comparisons with the timers rather than delays.

Current version of the code can be found on GitHub:  https://github.com/gcronin/GPSLockbox/blob/master/GPS_micro/GPS_micro.ino .

No comments:

Post a Comment