The Frustromantic Box, Part 4: Software

The final post in the Frustromantic Box series deals with the software side of things.

I’ll just let the code speak for itself, mostly.  I just want to say “thanks” to all the Arduino developers for the great libraries, and to Mikal Hart in particular for his work on the TinyGPS and NewSoftSerial libraries.

#include <TinyGPS.h>
#include <NewSoftSerial.h>
#include <LiquidCrystal.h>
#include <SoftwareServo.h>
#include <math.h>
#include <EEPROM.h>
#include <avr/interrupt.h>

/*
 * This code is in the public domain.
 */

#define RXPIN 2
#define TXPIN 3

#define POLULUPIN 5

#define LIDPIN 6
#define DRAWERPIN 4
#define IRPINA 0

#define EARTH_RADIUS 6378.1f
#define DEG_TO_RAD 0.0174532925f

#define TOFINO_LAT 49.122444f
#define TOFINO_LON 125.900871f

#define XXXX_LAT 0.0f
#define XXXX_LON 0.0f

#define HOME_LAT 64.0f
#define HOME_LON 128.0f

#define GPS_DELAY_MS 60000

/* 5km is pretty generous, but
   better safe than sorry, right? */
#define DIST_THRESHOLD 5.0f

#define ADDR_TRIES 0
#define ADDR_STAGE1 1
#define ADDR_STAGE2 2
#define ADDR_BACKDOOR 3

TinyGPS gps;
SoftwareServo drawerServo;
SoftwareServo lidServo;

/* Set up the NewSoftSerial to talk to the GPS */
NewSoftSerial nss(RXPIN,TXPIN);

LiquidCrystal lcd(7, 8, 9, 10, 11, 12);

unsigned long waitUntil;
int attemptsRemaining;

void setup() {
  pinMode(POLULUPIN, OUTPUT);
  Serial.begin(9600);
  Serial.print( "In setup.\n" );
  nss.begin(4800);
  lcd.begin(2,16);
  lcd.clear();
  lcd.setCursor(0,0);

  /* Check how many tries are left */
  attemptsRemaining = EEPROM.read(ADDR_TRIES);
  Serial.print( "Attempts Remaining: " );
  Serial.print( attemptsRemaining, DEC );
  Serial.print("\n");
  if ( attemptsRemaining == 0xFF )
  {
    // First time through
    Serial.print( "First time through, initializing EEPROM\n" );
    initializeEEPROM();
    Serial.print( "Locking lid\n" );
    lockLid();
    shutdown();
  }
  lcd.print( " ...Loading..." );
}

void loop()
{
  waitUntil = millis() + 2000;
  if ( waitForBackdoor() )
  {
    /* If we get here, then the backdoor was triggered */
    unlockLid();
    shutdown();
  }
  else if ( EEPROM.read( ADDR_BACKDOOR ) != 0 )
  {
    // clear the backdoor flag, if it was set from a previous
    // run.  this lets us try to unlock the backdoor multiple times
    // in case the first try jammed or something.
    lockLid();
    EEPROM.write( ADDR_BACKDOOR, 0 );
    shutdown();
  }

  if ( attemptsRemaining <= 0 )
  {
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print( " No More Tries!" );
    delay(5000);
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print( "   Return to" );
    lcd.setCursor(0,1);
    lcd.print( "     Russ" );
    delay(5000);
    shutdown();
  }

  // ... Normal Operation begins here
  bool stage1Complete = (EEPROM.read(ADDR_STAGE1) != 0);
  bool stage2Complete = (EEPROM.read(ADDR_STAGE2) != 0);

  waitUntil = millis() + 120000;
  if ( !stage1Complete )
  {
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print( " Searching  for " );
    lcd.setCursor(0,1);
    lcd.print( "     Signal     " );
    float stage1Dist = 100.0f;
    if ( distanceTo( TOFINO_LAT, TOFINO_LON, &stage1Dist ) )
    {
      /* If we get here, we received a valid distance measurement */
      if ( stage1Dist < DIST_THRESHOLD )
      {
        EEPROM.write( ADDR_STAGE1, 0xFF );
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print( "Stage One" );
        lcd.setCursor(0,1);
        lcd.print( "Complete!");
        unlockDrawer();
      }
      else
      {
        /* Sorry, try again */
        decrAttemptsRemaining();
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print( "Access Denied!" );
        lcd.setCursor(0,1);
        lcd.print( attemptsRemaining );
        lcd.print( " tries left" );
        delay(25000);
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print( "Out of Range" );
        lcd.setCursor(0,1);
        lcd.print( stage1Dist );
        lcd.print( " km" );
      }
      delay(25000);
      shutdown();
    }
    else
    {
      /* We never received a good GPS signal */
      failNoSignal();
    }
  }
  else if ( !stage2Complete )
  {
    /* This chunk of code is just different enough from stage 1
       to make it awkward to write in a loop. */
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print( " Searching  for " );
    lcd.setCursor(0,1);
    lcd.print( "     Signal     " );
    float stage2Dist = 100.0f;
    if ( distanceTo( XXXX_LAT, XXXX_LON, &stage2Dist ) )
    {
      if ( stage2Dist < DIST_THRESHOLD )
      {
        EEPROM.write( ADDR_STAGE2, 0xFF );
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print( "Stage Two" );
        lcd.setCursor(0,1);
        lcd.print( "Complete!" );
        unlockLid();
      }
      else
      {
        decrAttemptsRemaining();
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print( "Access Denied!" );
        lcd.setCursor(0,1);
        lcd.print( attemptsRemaining );
        lcd.print( " tries left" );
        delay(25000);
        lcd.clear();
        lcd.setCursor(0,0);
        lcd.print( "Out of Range" );
        lcd.setCursor(0,1);
        if ( stage2Dist < 10 )
        {
          lcd.print( "? km" );
        }
        else if ( stage2Dist < 100 )
        {
          lcd.print( "?? km" );
        }
        else if (stage2Dist < 1000 )
        {
          lcd.print( "??? km" );
        }
        else
        {
          lcd.print( "???? km" );
        }
      }
      delay(25000);
      shutdown();
    }
    else
    {
      failNoSignal();
    }
  }
  else
  {
    // Game Over!
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print( "      Game" );
    lcd.setCursor(0,1);
    lcd.print( "      Over!" );
    delay(10000);
    shutdown();
  }
  // We should never get here, but just in case...
  shutdown();
}

/*
 * Tries to calculate the distance to the specified lat+lon, using the GPS sensor.
 * Returns true iff the sensor returned a valid reading.
 * On successful return, result will hold the great circle distance to the specified point.
 */
bool distanceTo( float targetLat, float targetLon, float* result)
{
  int numFixes = 0;
  while ( millis() < waitUntil )
  {
    if (nss.available())
    {
      int c = nss.read();
      if (gps.encode(c))
      {
        unsigned long fix_age;
        float flat,flon;
        gps.f_get_position(&flat, &flon, &fix_age);
        if ( fix_age > 60000 ) continue;
        if ( numFixes < 5 )
        {
          numFixes++;
          continue;
        }
        flat = fabs(flat);
        flon = fabs(flon);

        float dist = gcd( flat * DEG_TO_RAD, flon * DEG_TO_RAD, targetLat * DEG_TO_RAD, targetLon * DEG_TO_RAD );
        *result = dist;
        return true;
      }
    }
  }
  return false;
}

/*
 * This is a super-kludge to turn off all pin-change interrupts.
 * It will disable all serial functionality.  Pretty much the
 * only way to restore the Arduino to a usable state after calling this
 * function is to cut the power and reboot.  Which is a little tricky to do
 * if you're running on USB _and_ a battery pack.
 *
 * I use this as a hack to get NewSoftSerial and SoftwareServo to play nice
 * with each other, but I think that the latest version of NewSoftSerial has
 * a much cleaner fix for this.
 */
void disablePCI()
{
  PCICR = 0;
  PCMSK2 = 0;
  PCMSK0 = 0;
  PCMSK1 = 0;
}

// Blocks until IR code received
// or expiry is reached (returns true iff code received)
// reads the waitUntil variable to determine how long
// to wait for the backdoor.
bool waitForBackdoor()
{
  uint8_t pulseCount = 0;
  unsigned long pulseStart;
  bool pulseActive;

  while ( millis() < waitUntil )
  {
    int irval = analogRead(IRPINA);
    if (irval < 0x0F)
    {
      // pulse started
      if ( !pulseActive )
      {
        pulseActive = true;
        pulseStart = millis();
      }
    }
    else
    {
      if ( pulseActive )
      {
        pulseActive = false;
        unsigned long duration = millis() - pulseStart;
        if ( (duration > 5) && (duration < 15) )
        {
          pulseCount++;
        }
      }
    }
  }
  /* 4 or more pulses within the duration specified by waitUntil will unlock the backdoor. */
  if (pulseCount >= 4)
  {
    EEPROM.write( ADDR_BACKDOOR, 0xFF );
    return true;
  }
  return false;
}

/*
 * This function only works when the Arduino is running off a battery pack.
 * When running on USB, maybe stick an infinite loop at the end?
 */
void shutdown()
{
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print( " Shutting Down.");
  delay(2000);
  digitalWrite( POLULUPIN, HIGH );
}

void unlockDrawer()
{
  // disablePCI _MUST_ be called prior to any servo operations!
  disablePCI();
  drawerServo.attach(DRAWERPIN);
  // this "stepping" is only necessary due to limitations of the Software Servo library,
  // which is an obsolete version of the Servo library that I used before
  // I diagnosed the problem with the pin change interrupts and never
  // had time to replace.
  for ( int i = 150; i >= 5; i-- )
  {
    drawerServo.write(i);
    SoftwareServo::refresh();
    delay(15);
  }
  drawerServo.detach();
}

void rotateLidServo(bool lock, int steps)
{
  disablePCI();
  // In my setup, the lid servo is a continuous rotation servo and so writing an angular
  // value to it makes it go a little crazy.
  lidServo.attach(LIDPIN);
  lidServo.write((lock) ? 0 : 180);
  for ( int i = 0; i < steps; i++ )
  {
    SoftwareServo::refresh();
    delayMicroseconds(15000);
  }
  lidServo.detach();
}

void unlockLid()
{
  // give it a bit more juice on the "unlock" just in case
  rotateLidServo(false, 10);
}

void lockLid()
{
  rotateLidServo(true, 7);
}

/*
 * Great Circle Distance calculation.  If anyone knows a fixed-point version
 * of this function I'd love to see it.
 */
float gcd(float lat_a, float lon_a, float lat_b, float lon_b)
{
  float d = acos(sin(lat_a)*sin(lat_b)+cos(lat_a)*cos(lat_b)*cos(lon_a-lon_b));
  return fabs(EARTH_RADIUS * d);
}

void decrAttemptsRemaining()
{
  attemptsRemaining--;
  // save the new state.
  EEPROM.write(ADDR_TRIES, attemptsRemaining);
}

void failNoSignal()
{
  decrAttemptsRemaining();
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print( "Access Denied!" );
  lcd.setCursor(0,1);
  lcd.print( attemptsRemaining );
  lcd.print( " tries left" );
  delay(5000);
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print( "No Signal!" );
  delay(5000);
  shutdown();
}

/* This function resets the box to its original state. */
void initializeEEPROM()
{
  // 101 Tries remaining
  EEPROM.write(ADDR_TRIES, 101);
  EEPROM.write(ADDR_STAGE1, 0);
  EEPROM.write(ADDR_STAGE2, 0);
  EEPROM.write(ADDR_BACKDOOR, 0);
}

6 comments to The Frustromantic Box, Part 4: Software

  • Scott

    I had written a function to do SIN and COS in fixed-point. It’s very simple, but nowhere near accurate enough for what you’re doing. I re-normalized the trig functions to use 256ths of a circle instead of degrees or radians, and return an 8-bit value 0-255 instead of 0-1. It used a simple 8-bit lookup table to determine the correct value. I calculated the return values for the table in a spreadsheet in OpenOffice. Not enough resolution and the technique is pretty useless in this situation, but perhaps it could spur an idea.

  • Chosen1

    What plugin are you using to display the code, that formats it so well?

  • Trent

    Hello from Australia,
    Awesome clone of Mikals box – I like the fact you made the box your self! I brought some parts today(including my first Arduino) to make one of these for my sisters wedding. I work on a mine site so I wont be able to get my hands on it to play around until I get back home unfortunately. This is my first Arduino project so the learning curve is going to be steep. Can you please point me in the direction of where to get the correct libraries from? I want to do as much my self as I can but I only have a month before the wedding and working away from home makes this difficult. Your project is very well documented:)

  • admin

    Hi, Trent.

    Good luck! This was my second Arduino project. It was a steep learning curve, but manageable. The two most important libraries are TinyGPS and NewSoftSerial, both of which were created by Mikal Hart.

    I used the SoftwareServo library, but if I were to do it again I’d use the servo library that comes with the most recent version of the Arduino software.

  • splargle

    Just a thought, it could be fun to implement the frustromantic box as a mobile web application using the Geolocation API’s supported by several browsers now, including iPhone and ones available for android.

    It would be kind of a neat idea to post a message or image online that someone can only view when they’ve visited a certain location.

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>