Michael Gillespie
Published

Spot

A smart GPS watch that helps you set and find personal landmarks.

Full instructions provided970
Spot

Things used in this project

Hardware components

MicroView
OLED screen with embedded controller
×1
MicroView Programmer
×1
Adafruit Ultimate GPS
×1
Sparkfun HMC5883L Magnetometer
×1
Tactile pushbuttons
×4
10K resistors
×4

Story

Read more

Code

SpotWatch.ino

C/C++
This is the newer code, GPS + MicroView
/***************************************************************************
  This is a library example for the HMC5883 magnentometer/compass

  Designed specifically to work with the Adafruit HMC5883 Breakout
  http://www.adafruit.com/products/1746
 
  *** You will also need to install the Adafruit_Sensor library! ***

  These displays use I2C to communicate, 2 pins are required to interface.

  Adafruit invests time and resources providing this open source code,
  please support Adafruit andopen-source hardware by purchasing products
  from Adafruit!

  Written by Kevin Townsend for Adafruit Industries with some heading example from
  Love Electronics (loveelectronics.co.uk)
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the version 3 GNU General Public License as
 published by the Free Software Foundation.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.

 ***************************************************************************/
#include <MicroView.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_HMC5883_U.h>

#define WHEEL_SIZE 23

const uint8_t maxW = uView.getLCDWidth();
const uint8_t midW = maxW/2;
const uint8_t maxH = uView.getLCDHeight();
const uint8_t midH = maxH/2;

/* Assign a unique ID to this sensor at the same time */
Adafruit_HMC5883_Unified mag = Adafruit_HMC5883_Unified(12345);

void displaySensorDetails(void)
{
  sensor_t sensor;
  mag.getSensor(&sensor);
  Serial.println("------------------------------------");
  Serial.print  ("Sensor:       "); Serial.println(sensor.name);
  Serial.print  ("Driver Ver:   "); Serial.println(sensor.version);
  Serial.print  ("Unique ID:    "); Serial.println(sensor.sensor_id);
  Serial.print  ("Max Value:    "); Serial.print(sensor.max_value); Serial.println(" uT");
  Serial.print  ("Min Value:    "); Serial.print(sensor.min_value); Serial.println(" uT");
  Serial.print  ("Resolution:   "); Serial.print(sensor.resolution); Serial.println(" uT");  
  Serial.println("------------------------------------");
  Serial.println("");
  delay(500);
}

void setup(void) 
{
  Serial.begin(9600);
  Serial.println("HMC5883 Magnetometer Test"); Serial.println("");
  
  /* Initialise the sensor */
  if(!mag.begin())
  {
    /* There was a problem detecting the HMC5883 ... check your connections */
    Serial.println("Ooops, no HMC5883 detected ... Check your wiring!");
    while(1);
  }
  
  uView.begin();    // set up the MicroView
  uView.clear(PAGE);// erase hardware memory inside the OLED
  uView.display();  // display the content in the buffer

  
  
  /* Display some basic information on this sensor */
  displaySensorDetails();
}

void loop(void) 
{
  /* Get a new sensor event */ 
  sensors_event_t event; 
  mag.getEvent(&event);
 
  /* Display the results (magnetic vector values are in micro-Tesla (uT)) */
  Serial.print("X: "); Serial.print(event.magnetic.x); Serial.print("  ");
  Serial.print("Y: "); Serial.print(event.magnetic.y); Serial.print("  ");
  Serial.print("Z: "); Serial.print(event.magnetic.z); Serial.print("  ");Serial.println("uT");

  // Hold the module so that Z is pointing 'up' and you can measure the heading with x&y
  // Calculate heading when the magnetometer is level, then correct for signs of axis.
  float heading = atan2(event.magnetic.y, event.magnetic.x);
  
  // Once you have your heading, you must then add your 'Declination Angle', which is the 'Error' of the magnetic field in your location.
  // Find yours here: http://www.magnetic-declination.com/
  // Mine is: -13* 2' W, which is ~13 Degrees, or (which we need) 0.22 radians
  // If you cannot find your Declination, comment out these two lines, your compass will be slightly off.
  float declinationAngle = 0.17;
  heading += declinationAngle;
  
  // Correct for when signs are reversed.
  if(heading < 0)
    heading += 2*PI;
    
  // Check for wrap due to addition of declination.
  if(heading > 2*PI)
    heading -= 2*PI;
   
  // Convert radians to degrees for readability.
  float headingDegrees = heading * 180/M_PI; 
  
  Serial.print("Heading (degrees): "); Serial.println(headingDegrees);
  drawFace();
  drawTime(headingDegrees);
  delay(500);
  uView.display();
}

void drawFace()
{
  uView.setFontType(0); // set font type 0 (Smallest)
  
  uint8_t fontW = uView.getFontWidth();
  uint8_t fontH = uView.getFontHeight();
  
  //uView.setCursor(27, 0); // points cursor to x=27 y=0
  uView.setCursor(midW-(fontW/2)-1, midH-WHEEL_SIZE+1);
  uView.print("|");  // Print the "12"
  uView.setCursor(midW-(fontW/2)-1, midH+WHEEL_SIZE-fontH-1);
  uView.print("|");  // Print the "6"
  uView.setCursor(midW-WHEEL_SIZE+1, midH-fontH/2);
  uView.print("-");  // Print the "9"
  uView.setCursor(midW+WHEEL_SIZE-fontW-2, midH-fontH/2);
  uView.print("-");  // Print the "3"
  uView.circle(midW-1, midH-1, WHEEL_SIZE);
  //Draw the clock
  uView.display();
}

void drawTime(float inputDegrees)
{
  static boolean firstDraw = false;
  static float degresssec, secx, secy;
  // If mSec
  
  
    // First time draw requires extra line to set up XOR's:
    if (firstDraw) 
    {
      uView.line(midW, midH, 32 + secx, 24 + secy, WHITE, XOR);
    }
    degresssec = (((inputDegrees * 360) / 60) + 270) * (PI / 180);

    // Calculate x,y coordinates of second hand:
    secx = cos(degresssec) * (WHEEL_SIZE / 1.1);
    secy = sin(degresssec) * (WHEEL_SIZE / 1.1);

    // Draw hands with the line function:
    uView.line(midW, midH, midW+secx, midH+secy, WHITE, XOR);
    
    // Set firstDraw flag to true, so we don't do it again.
    firstDraw = true;
    
    // Actually draw the hands with the display() function.
    uView.display();
  
}

MicroView_GPSWatch.ino

C/C++
This is the code from the hackathon.
/*
	MicroView Arduino Library
	Copyright (C) 2014 GeekAmmo

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include <MicroView.h>
#include <Adafruit_GPS.h>
#include <SoftwareSerial.h>
#include <Time.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_HMC5883_U.h>
#include <Adafruit_NeoPixel.h>

SoftwareSerial mySerial(3, 2); // RX, TX


Adafruit_HMC5883_Unified mag = Adafruit_HMC5883_Unified(12345);
Adafruit_GPS GPS(&mySerial);

#define WHEEL_SIZE 23
#define GPSECHO false
#define SECOND 00

//--------------------------------------------------|
//                    WAYPOINT                      |
//--------------------------------------------------|
//Please enter the latitude and longitude of your   |
//desired destination:                              |
#define GEO_LAT                48.009551
#define GEO_LON               -88.771131
//--------------------------------------------------|
//Your compass module may not line up with ours.    |
//Once you run compass mode, compare to a separate  |
//compass (like one found on your smartphone).      |
//Point your TOP_LED north, then count clockwise    |
//how many LEDs away from TOP_LED the lit LED is    |
#define LED_OFFSET             0
//--------------------------------------------------|
float fLat = 0.0;
float fLon = 0.0;
int currentGoal = 0;
int goalCount = 0;
float Lat[6];
float Lon[6];
char names[6];
char letters[6] = {'A', 'B', 'C', 'D', 'E', 'F'};
int totalLocs = 6;

const uint8_t maxW = uView.getLCDWidth();
const uint8_t midW = maxW/2;
const uint8_t maxH = uView.getLCDHeight();
const uint8_t midH = maxH/2;
// Navigation location
float targetLat = Lat[0];
float targetLon = Lon[0];
// this keeps track of whether we're using the interrupt
// off by default!
boolean usingInterrupt = false;
// Trip distance
float tripDistance;
// Offset hours from gps time (UTC)
//const int offset = 1;   // Central European Time
//const int offset = -4;  // Eastern Daylight Time (USA)
//const int offset = -5;  // Central Daylight Time (USA)
//const int offset = -8;  // Pacific Standard Time (USA)
const int offset = -7;  // Pacific Daylight Time (USA)
int compassOffset = LED_OFFSET;
int lastMin = 16;
int lastHour = 16;
int startLED = 0;
int startLEDlast = 16;
int lastCombined = 0;
int start = 0;
int mode = 0;
int lastDir = 16;
int dirLED_r = 0;
int dirLED_g = 0;
int dirLED_b = 255;
int compassReading;

// Calibration offsets
float magxOffset = 2.55;
float magyOffset = 27.95;


// Pushbutton setup
int button1 = 6; // the number of the pushbutton pin
int button2 = 5;
int button3 = A1;
int button4 = A0;
int buttonState;               
int lastButtonState = HIGH;    
long buttonHoldTime = 0;         // the last time the output pin was toggled
long buttonHoldDelay = 250;      // how long to hold the button down

// the following variables are long's because the time, measured in miliseconds,
// will quickly become a bigger number than can be stored in an int.
long lastDebounceTime = 0;     // the last time the output pin was toggled
long debounceDelay = 50;       // the debounce time; increase if the output flickers
long menuDelay = 250;
long menuTime;



void setup() {
	// Set the time in the time library:
  // connect at 115200 so we can read the GPS fast enough and echo without dropping chars
  // also spit it out
  Serial.begin(115200);
  Serial.println("Adafruit GPS library basic test!");

  // 9600 NMEA is the default baud rate for Adafruit MTK GPS's- some use 4800
  GPS.begin(9600);
  // uncomment this line to turn on RMC (recommended minimum) and GGA (fix data) including altitude
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  // uncomment this line to turn on only the "minimum recommended" data
  //GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCONLY);
  // For parsing data, we don't suggest using anything but either RMC only or RMC+GGA since
  // the parser doesn't care about other sentences at this time
  // Set the update rate
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); // 1 Hz update rate
  // For the parsing code to work nicely and have time to sort thru the data, and
  // print it out we don't suggest using anything higher than 1 Hz

  // Request updates on antenna status, comment out to keep quiet
  GPS.sendCommand(PGCMD_ANTENNA);
  
   // Make input & enable pull-up resistors on switch pins for pushbutton
  pinMode(button1, INPUT);
  pinMode(button2, INPUT);
  pinMode(button3, INPUT);
  pinMode(button4, INPUT);

  uView.begin();    // set up the MicroView
  uView.clear(PAGE);// erase hardware memory inside the OLED
  uView.display();  // display the content in the buffer

  // Draw clock face (circle outline & text):
  drawFace();
}
uint32_t gpsTimer = millis();
uint32_t startupTimer = millis();
uint32_t compassTimer = millis();
void loop() {
 //    Serial.println(digitalRead());   
  Serial.println(compassReading);
  // read the state of the switch into a local variable:
  
  //Serial.println(buttonState);
  // read data from the GPS in the 'main loop'
  char c = GPS.read();
  // if you want to debug, this is a good time to do it!
  if (GPSECHO)
    if (c) Serial.print(c);
  // if a sentence is received, we can check the checksum, parse it...
  if (GPS.newNMEAreceived()) {
    // a tricky thing here is if we print the NMEA sentence, or data
    // we end up not listening and catching other sentences!
    // so be very wary if using OUTPUT_ALLDATA and trytng to print out data
    Serial.println(GPS.lastNMEA()); // this also sets the newNMEAreceived() flag to false
    if (!GPS.parse(GPS.lastNMEA())) // this also sets the newNMEAreceived() flag to false
      return; // we can fail to parse a sentence in which case we should just wait for another
  }

  // if millis() or timer wraps around, we'll just reset it
  if (gpsTimer > millis()) gpsTimer = millis();

  if (start == 0) {
    if (GPS.fix) {
      // set the Time to the latest GPS reading
      setTime(GPS.hour, GPS.minute, GPS.seconds, GPS.day, GPS.month, GPS.year);
      delay(50);
      adjustTime(offset * SECS_PER_HOUR);
      delay(500);
      tripDistance = (double)calc_dist(fLat, fLon, targetLat, targetLon);
      start = 1;
    }
  }    
  // approximately every 60 seconds or so, update time
  if ((millis() - gpsTimer > 60000) && (start == 1)) {
    gpsTimer = millis(); // reset the timer
    if (GPS.fix) {
      // set the Time to the latest GPS reading
      setTime(GPS.hour, GPS.minute, GPS.seconds, GPS.day, GPS.month, GPS.year);
      delay(50);
      adjustTime(offset * SECS_PER_HOUR);
      delay(500);
    }
  }

  if (GPS.fix) {
    fLat = decimalDegrees(GPS.latitude, GPS.lat);
    fLon = decimalDegrees(GPS.longitude, GPS.lon);
  }
  
  //placesMode();
  if(digitalRead(button4) == HIGH){
    setMode();
  } 
}

void drawTime(float inputDegrees)
{
  static boolean firstDraw = false;
  static float degresssec, secx, secy;
  // If mSec
  
  
    // First time draw requires extra line to set up XOR's:
    if (firstDraw) 
    {
      uView.line(midW, midH, 32 + secx, 24 + secy, WHITE, XOR);
    }
    degresssec = (((inputDegrees * 360) / 60) + 270) * (PI / 180);

    // Calculate x,y coordinates of second hand:
    secx = cos(degresssec) * (WHEEL_SIZE / 1.1);
    secy = sin(degresssec) * (WHEEL_SIZE / 1.1);

    // Draw hands with the line function:
    uView.line(midW, midH, midW+secx, midH+secy, WHITE, XOR);
    
    // Set firstDraw flag to true, so we don't do it again.
    firstDraw = true;
    
    // Actually draw the hands with the display() function.
    uView.display();
  
}

// Draw the clock face. That includes the circle outline and
// the 12, 3, 6, and 9 text.
void drawFace()
{
  uView.setFontType(0); // set font type 0 (Smallest)
  
  uint8_t fontW = uView.getFontWidth();
  uint8_t fontH = uView.getFontHeight();
  
  //uView.setCursor(27, 0); // points cursor to x=27 y=0
  uView.setCursor(midW-(fontW/2)-1, midH-WHEEL_SIZE+1);
  uView.print("|");  // Print the "12"
  uView.setCursor(midW-(fontW/2)-1, midH+WHEEL_SIZE-fontH-1);
  uView.print("|");  // Print the "6"
  uView.setCursor(midW-WHEEL_SIZE+1, midH-fontH/2);
  uView.print("-");  // Print the "9"
  uView.setCursor(midW+WHEEL_SIZE-fontW-2, midH-fontH/2);
  uView.print("-");  // Print the "3"
  uView.circle(midW-1, midH-1, WHEEL_SIZE);
  //Draw the clock
  uView.display();
}
boolean finish = false;
void setMode() { 
  while(finish == false){
    float currentLat = decimalDegrees(GPS.latitude, GPS.lat);
    float currentLon = decimalDegrees(GPS.longitude, GPS.lon);
    Lat[goalCount] = currentLat;
    Lon[goalCount] = currentLon;
    names[goalCount] = letters[goalCount];
    uView.clear(PAGE);
    uView.setCursor(midW-1-20, midH-1);
    uView.print("Set as ");
    uView.print(names[goalCount]);
    uView.display();
    delay(2000);
    if(digitalRead(button3)== HIGH){
      finish = true;
      return;
    }
    
    }
   delay(100);
   finish = true;
  
}

void placesMode(){
  boolean finished = false;
  while(finished == false){  
    uView.clear(PAGE);
    int rowCount = 0;
    if(digitalRead(button2) == HIGH){
      rowCount++;
      uView.clear(PAGE);
      for(totalLocs; totalLocs < 5; totalLocs++){
        uView.setCursor(10, totalLocs);
        uView.print(names[totalLocs]);
      }
      uView.setCursor(0,rowCount*10);
      uView.print(">");
      uView.display();
    }
    else if(digitalRead(button3) == HIGH){
      rowCount--;
      uView.clear(PAGE);
      for(totalLocs; totalLocs < 5; totalLocs++){
        uView.setCursor(10, totalLocs);
        uView.print(names[totalLocs]);
      }
      uView.setCursor(0,rowCount);
      uView.print(">");
      uView.display();
    }
    else if(digitalRead(button1) == HIGH){
      uView.clear(PAGE);
      for(totalLocs; totalLocs < 5; totalLocs++){
        uView.setCursor(10, totalLocs);
        uView.print(names[totalLocs]);
      }
      uView.setCursor(0,rowCount);
      uView.print(">");
      uView.display();
      targetLat = Lat[rowCount];
      targetLon = Lon[rowCount];
      navMode();
      finished = true;
    }
    else {
      uView.clear(PAGE);
      for(totalLocs; totalLocs < 5; totalLocs++){
        uView.setCursor(10, totalLocs);
        uView.print(names[totalLocs]);
        uView.display();
      }
    }
  }
}

void navMode() {
  boolean finishy = false;
  while(finishy == false){
    if (start == 1) {
  
      compassCheck();
  
      if ((calc_bearing(fLat, fLon, targetLat, targetLon) - compassReading) > 0) {
        drawTime(calc_bearing(fLat, fLon, targetLat, targetLon)-compassReading);
      } 
      else {
        drawTime(calc_bearing(fLat, fLon, targetLat, targetLon)-compassReading+360);
      }
  
    } 
    else {
      // if millis() or timer wraps around, we'll just reset it
      if (startupTimer > millis()) startupTimer = millis();
  
      // approximately every 10 seconds or so, update time
      if (millis() - startupTimer > 200) {
        startupTimer = millis(); // reset the timer
        
      }
    }
    uView.display();
    
  }
  
}

int calc_bearing(float flat1, float flon1, float flat2, float flon2)
{
  float calc;
  float bear_calc;

  float x = 69.1 * (flat2 - flat1); 
  float y = 69.1 * (flon2 - flon1) * cos(flat1/57.3);

  calc=atan2(y,x);

  bear_calc= degrees(calc);

  if(bear_calc<=1){
    bear_calc=360+bear_calc; 
  }
  return bear_calc;
}


unsigned long calc_dist(float flat1, float flon1, float flat2, float flon2)
{
  float dist_calc=0;
  float dist_calc2=0;
  float diflat=0;
  float diflon=0;

  diflat=radians(flat2-flat1);
  flat1=radians(flat1);
  flat2=radians(flat2);
  diflon=radians((flon2)-(flon1));

  dist_calc = (sin(diflat/2.0)*sin(diflat/2.0));
  dist_calc2= cos(flat1);
  dist_calc2*=cos(flat2);
  dist_calc2*=sin(diflon/2.0);
  dist_calc2*=sin(diflon/2.0);
  dist_calc +=dist_calc2;

  dist_calc=(2*atan2(sqrt(dist_calc),sqrt(1.0-dist_calc)));

  dist_calc*=6371000.0; //Converting to meters
  return dist_calc;
}

// Convert NMEA coordinate to decimal degrees
float decimalDegrees(float nmeaCoord, char dir) {
  uint16_t wholeDegrees = 0.01*nmeaCoord;
  int modifier = 1;

  if (dir == 'W' || dir == 'S') {
    modifier = -1;
  }

  return (wholeDegrees + (nmeaCoord - 100.0*wholeDegrees)/60.0) * modifier;
}


void compassCheck() {
  // if millis() or timer wraps around, we'll just reset it
  if (compassTimer > millis()) compassTimer = millis();

  // approximately every 10 seconds or so, update time
  if (millis() - compassTimer > 50) {
    /* Get a new sensor event */
    sensors_event_t event; 
    mag.getEvent(&event);

    float Pi = 3.14159;

    compassTimer = millis(); // reset the timer

    // Calculate the angle of the vector y,x
    float heading = (atan2(event.magnetic.y + magyOffset,event.magnetic.x + magxOffset) * 180) / Pi;

    // Normalize to 0-360
    if (heading < 0)
    {
      heading = 360 + heading;
    }
    compassReading = heading; 
    Serial.println(compassReading);
  }  
}  

Credits

Michael Gillespie

Michael Gillespie

1 project • 3 followers

Comments