Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Brian Rashap
Published © MIT

IoT Pizza Finder

This IoT Pizza Finder is a class assignment for the CNM IoT workforce development course to teach to integrate the web into IoT devices.

AdvancedFull instructions provided6 hours1,968
IoT Pizza Finder

Things used in this project

Story

Read more

Schematics

Pizza Finder schematic

Designed in KiCad

Pizza Finder PCB

Designed in KiCad

Code

PizzaFinder.ino

C/C++
Particle code for Pizza Finder.
Note: MQTT and TomTom credentials stored in credentials.h and for obvious reasons not posed here.
/*
 * Project PizzaFinder
 * Description: Particle code that interfaces with Node-Red (via MQTT) to find Pizza
 * Author: Brian Rashap
 * Date: 18-May-2022
 */

#include <Adafruit_MQTT.h>
#include "Adafruit_MQTT/Adafruit_MQTT.h"
#include "Adafruit_MQTT/Adafruit_MQTT_SPARK.h"
#include "credentials.h"
#include "Adafruit_GFX.h"
#include "Adafruit_SSD1306.h"
#include "Adafruit_GPS.h"
#include "math.h"
#include "Button.h"

/************ Global State (you don't need to change this!) ******************/ 
TCPClient TheClient; 

// Setup the MQTT client class by passing in the WiFi client and MQTT server and login details. 
Adafruit_MQTT_SPARK mqtt(&TheClient,AIO_SERVER,AIO_SERVERPORT,AIO_USERNAME,AIO_KEY); 

/****************************** Feeds ***************************************/
Adafruit_MQTT_Publish findPizza = Adafruit_MQTT_Publish(&mqtt,"pizza/query");
Adafruit_MQTT_Subscribe foundPizza = Adafruit_MQTT_Subscribe(&mqtt,"pizza/found");
Adafruit_MQTT_Subscribe foundPizzalat = Adafruit_MQTT_Subscribe(&mqtt,"pizza/lat");
Adafruit_MQTT_Subscribe foundPizzalon = Adafruit_MQTT_Subscribe(&mqtt,"pizza/lon");

/**** Configure Display, GPS, Button Objects *******/
Adafruit_SSD1306 display(-1);
Adafruit_GPS GPS(&Wire);
Button getPizza(D9);

// Define Constants
const int TIMEZONE = -6;
const int BUTTONPIN = D8; 
const int DISPLAYUPDATE = 5000;

// Declare Variables
String query;
unsigned int lastTime;
float pizzalat, pizzalon;
float myLat, myLon, myAlt;
int sat;
unsigned int lastGPS;
float heading, distance;

// Declare Functions
void helloDisplay();
void GPSbegin();
void getGPS(float *latitude, float *longitude, float *altitude, int *satellites);
float getDistance(float lat1, float lon1, float lat2, float lon2);
float getHeading(float lat1, float lon1, float lat2, float lon2);
void MQTT_connect();
bool MQTT_ping();

void setup() {
  Serial.begin(9600);
  waitFor(Serial.isConnected,5000);
  delay(1000);

  pinMode(D7,OUTPUT);
  digitalWrite(D7,LOW);

  // set initial for first query
  myLat = 35.084250;
  myLon = -106.649240;
  query = "key="+key+"&Lat="+myLat+"&Lon="+myLon+"&limit=1";
  Serial.printf("Query = %s\n",query.c_str());
  myLat = 0; //reset until GPS fix

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  helloDisplay();
  GPSbegin();

  mqtt.subscribe(&foundPizza);
  mqtt.subscribe(&foundPizzalat);
  mqtt.subscribe(&foundPizzalon);

  lastTime = -99999;
}


void loop() {
  MQTT_connect();
  MQTT_ping();

  // Get data from GSP unit (best if you do this continuously)
  GPS.read();
  if (GPS.newNMEAreceived()) {
    if (!GPS.parse(GPS.lastNMEA())) {
      return;
    }   
  }

  getGPS(&myLat,&myLon,&myAlt,&sat);


  if(getPizza.isClicked()) {
    lastTime = millis();
    Serial.printf("Requesting Pizza Location\n");    
    if(myLat != 0) {
      query = "key="+key+"&lat="+myLat+"&lon="+myLon+"&limit=1";
    }
    findPizza.publish(query);

    Adafruit_MQTT_Subscribe *subscription;
    while ((subscription = mqtt.readSubscription(2000))) {
      if (subscription == &foundPizza) {
        Serial.printf("Closest pizza is at %s\n",(char *)foundPizza.lastread);
        Serial.printf("Located at: %0.6f, %0.6f\n",pizzalat,pizzalon);
      }
      if (subscription == &foundPizzalat) {
        pizzalat = atof((char *)foundPizzalat.lastread);
      }
      if (subscription == &foundPizzalon) {
        pizzalon = atof((char *)foundPizzalon.lastread);
      }
    }
  }

  if(millis()-lastTime > DISPLAYUPDATE) {
    lastTime=millis();
    display.clearDisplay();
    display.setCursor(0,0);
    if(pizzalat == 0) {
      display.printf("Press Button to find Pizza\n");
    }
    else {
      display.printf("%s\n",(char *)foundPizza.lastread);
      display.printf("%0.5f, %0.5f\n",pizzalat,pizzalon);
      distance = getDistance(pizzalat,pizzalon,myLat,myLon);
      heading = getHeading(pizzalat,pizzalon,myLat,myLon);
      //Serial.printf("Distance: %0.2f meters\n",distance);
      display.printf("Distance: %0.0f m\n",distance);
      //Serial.printf("Heading: %0.2f \n",heading);
      display.printf("Heading: %0.0f%c\n",heading,0xF8);
    }
    display.display();
  }
}

// Print HelloWorld to display
void helloDisplay() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.printf("Hello\nWorld!\n");
  display.display();
  display.setTextSize(1);
}

//Initialize GPS
void GPSbegin() {
  GPS.begin(0x10);  // The I2C address to use is 0x10
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); 
  GPS.sendCommand(PGCMD_ANTENNA);
  delay(1000);
  GPS.println(PMTK_Q_RELEASE);
}


void getGPS(float *latitude, float *longitude, float *altitude, int *satellites){
  int theHour;

  theHour = GPS.hour + TIMEZONE;
  if(theHour < 0) {
    theHour = theHour + 24;
  }
    
  // Serial.printf("Time: %02i:%02i:%02i:%03i\n",theHour, GPS.minute, GPS.seconds, GPS.milliseconds);
  // Serial.printf("Dates: %02i-%02i-20%02i\n", GPS.month, GPS.day, GPS.year);
  // Serial.printf("Fix: %i, Quality: %i\n",(int)GPS.fix,(int)GPS.fixquality);
  if (GPS.fix) {
    *latitude = GPS.latitudeDegrees;
    *longitude = GPS.longitudeDegrees; 
    *altitude = GPS.altitude;
    *satellites = (int)GPS.satellites;
  }
  if(*satellites >= 4) {
    digitalWrite(D7,HIGH);
  }
  else {
    digitalWrite(D7,LOW);
  }
}


float getDistance(float lat1, float lon1, float lat2, float lon2) {
  float distance;
  //Serial.printf("Lat1 = %0.6f, Lat2 = %0.6f\n",lat1,lat2);
  distance = sqrt(pow(lat2-lat1,2)+pow(lon2-lon1,2));
  //Serial.printf("Distance %0.6f\n",distance);
  return distance*111139; 
}

float getHeading(float lat1, float lon1, float lat2, float lon2) {
  float heading;
  //Serial.printf("Lat1 = %0.6f, Lat2 = %0.6f\n",lat1,lat2);
  heading = atan2(lon1-lon2,lat1-lat2);
  //Serial.printf("Heading %0.6f\n",heading*(360/(2*M_PI)));
  return heading*(360/(2*M_PI));
}

void MQTT_connect() {
  int8_t ret;
 
  // Stop if already connected.
  if (mqtt.connected()) {
    return;
  }
 
  Serial.print("Connecting to MQTT... ");
 
  while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
       Serial.printf("%s\n",(char *)mqtt.connectErrorString(ret));
       Serial.printf("Retrying MQTT connection in 5 seconds..\n");
       mqtt.disconnect();
       delay(5000);  // wait 5 seconds
  }
  Serial.printf("MQTT Connected!\n");
}

bool MQTT_ping() {
  static unsigned int last;
  bool pingStatus;

  if ((millis()-last)>120000) {
      Serial.printf("Pinging MQTT \n");
      pingStatus = mqtt.ping();
      if(!pingStatus) {
        Serial.printf("Disconnecting \n");
        mqtt.disconnect();
      }
      last = millis();
  }
  return pingStatus;
}

Button.h

C Header File
An IoT button class created earlier in the IoT bootcamp
#ifndef _BUTTON_H_
#define _BUTTON_H_

class Button {
  int _buttonPin;
  int _prevButtonState;

  public:
    Button(int buttonPin) {
      _buttonPin = buttonPin;
      pinMode(_buttonPin,INPUT_PULLDOWN);
    }

    bool isPressed() {
      return digitalRead(_buttonPin);
    }

    bool isClicked() {
      bool _buttonState, _clicked;

      _buttonState = digitalRead(_buttonPin);
      if(_buttonState != _prevButtonState) {
        _clicked = _buttonState;
      }
      else {
        _clicked = false;
      }
      _prevButtonState=_buttonState;
      return _clicked;
    }
};

#endif // _BUTTON_H_

flows.json

JSON
Node-Red code for the flow shown above
[
    {
        "id": "0565f08319c13a77",
        "type": "tab",
        "label": "PizzaFinder",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "fe21466bb6d3d0ff",
        "type": "inject",
        "z": "0565f08319c13a77",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "key=sbBfSkyYhkwCoSk3Nb0AROqV1AA9KwA1&lat=35.084250&lon=-106.649240&limit=1",
        "payloadType": "str",
        "x": 390,
        "y": 120,
        "wires": [
            [
                "a067837cdf890505"
            ]
        ]
    },
    {
        "id": "527e7983cb085fcf",
        "type": "debug",
        "z": "0565f08319c13a77",
        "name": "Pizza Output",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "lat",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1090,
        "y": 180,
        "wires": []
    },
    {
        "id": "a067837cdf890505",
        "type": "http request",
        "z": "0565f08319c13a77",
        "name": "",
        "method": "GET",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "https://api.tomtom.com/search/2/search/pizza.json?{{{payload}}}",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 630,
        "y": 180,
        "wires": [
            [
                "87cb8ac9c510f04e",
                "c64ac9137ec3f1e4",
                "4d0673ab73aaf287",
                "d17c82e1b594d39c",
                "31d17200243df54f"
            ]
        ]
    },
    {
        "id": "87cb8ac9c510f04e",
        "type": "function",
        "z": "0565f08319c13a77",
        "name": "PizzaParser",
        "func": "var payload=msg.payload.results[0].position;\nmsg.lat = payload.lat;\nmsg.lon = payload.lon;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 850,
        "y": 180,
        "wires": [
            [
                "527e7983cb085fcf",
                "c6f385fc2a92f202"
            ]
        ]
    },
    {
        "id": "c6f385fc2a92f202",
        "type": "debug",
        "z": "0565f08319c13a77",
        "name": "Pizza Output",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "lon",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1090,
        "y": 220,
        "wires": []
    },
    {
        "id": "0f808d488380f267",
        "type": "mqtt in",
        "z": "0565f08319c13a77",
        "name": "PizzaQuery",
        "topic": "pizza/query",
        "qos": "2",
        "datatype": "auto-detect",
        "broker": "85afdc3425590ad8",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 370,
        "y": 200,
        "wires": [
            [
                "a067837cdf890505",
                "ff7e1c2f32331830"
            ]
        ]
    },
    {
        "id": "ff7e1c2f32331830",
        "type": "debug",
        "z": "0565f08319c13a77",
        "name": "Query",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 610,
        "y": 260,
        "wires": []
    },
    {
        "id": "f1f510a380f17a77",
        "type": "mqtt out",
        "z": "0565f08319c13a77",
        "name": "PizzaLocationLat",
        "topic": "pizza/lat",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "85afdc3425590ad8",
        "x": 1150,
        "y": 320,
        "wires": []
    },
    {
        "id": "c64ac9137ec3f1e4",
        "type": "function",
        "z": "0565f08319c13a77",
        "name": "PizzaParserLAT",
        "func": "var payload=msg.payload.results[0].position;\nmsg.payload=payload.lat;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 860,
        "y": 320,
        "wires": [
            [
                "f1f510a380f17a77",
                "dfda90a0b7fc167a"
            ]
        ]
    },
    {
        "id": "dfda90a0b7fc167a",
        "type": "debug",
        "z": "0565f08319c13a77",
        "name": "JSONLocation",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1140,
        "y": 380,
        "wires": []
    },
    {
        "id": "31d17200243df54f",
        "type": "function",
        "z": "0565f08319c13a77",
        "name": "PizzaParserName",
        "func": "var payload=msg.payload.results[0].poi.name;\nmsg.payload=payload;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 870,
        "y": 540,
        "wires": [
            [
                "dfda90a0b7fc167a",
                "bec343f585242f20"
            ]
        ]
    },
    {
        "id": "4d0673ab73aaf287",
        "type": "function",
        "z": "0565f08319c13a77",
        "name": "PizzaParserLON",
        "func": "var payload=msg.payload.results[0].position;\nmsg.payload=payload.lon;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 860,
        "y": 440,
        "wires": [
            [
                "759ce4ceed064e64",
                "dfda90a0b7fc167a"
            ]
        ]
    },
    {
        "id": "759ce4ceed064e64",
        "type": "mqtt out",
        "z": "0565f08319c13a77",
        "name": "PizzaLocationLon",
        "topic": "pizza/lon",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "85afdc3425590ad8",
        "x": 1150,
        "y": 440,
        "wires": []
    },
    {
        "id": "d17c82e1b594d39c",
        "type": "debug",
        "z": "0565f08319c13a77",
        "name": "PizzaFound",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 850,
        "y": 100,
        "wires": []
    },
    {
        "id": "bec343f585242f20",
        "type": "mqtt out",
        "z": "0565f08319c13a77",
        "name": "PizzaFound",
        "topic": "pizza/found",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "85afdc3425590ad8",
        "x": 1130,
        "y": 540,
        "wires": []
    },
    {
        "id": "85afdc3425590ad8",
        "type": "mqtt-broker",
        "name": "ddciot.us",
        "broker": "mqtt.ddciot.us",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]

Credits

Brian Rashap

Brian Rashap

12 projects • 107 followers
Former General Manager of US Facilities Operations at Intel Corporation. Currently loving my encore career as a teacher focused on IoT.

Comments