Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 |
Central New Mexico Community College's IoT Bootcamp is focused on providing non-traditional workforce development training to individuals looking to get into a technology career.
As part of our learning, we explore various ways to connect IoT devices to the Cloud both to send and receive data. The below project is inspired by a Pizza Compass project that Joe Grand did for Hackaday back in 2021. Joe does an amazing (and entertaining job) of walking through the functional and hardware design. In the class, we create our own twist on Joe's work by having our microcontroller query a Node-Red flow that then queries TomTom*** to retrieve coordinate.
*** NOTE: many APIs are now requiring a credit card, even to utilize their free services. This, for obvious reasons, does not work in a class environment. TomTom was selected as they allow basic queries without the need of providing a credit card. Thank you TomTom from students and educators everywhere.
The project reuses a PCB created for another class project that teaches GPS and LoRa communication.
The LoRa module is not needed for the Pizza Finder, so a button was placed into those PCB locations. And, the solar power connection is not used. Otherwise, it takes advantage of integrated GPS module and I2C interface for a small OLED display.
The Pizza Finder is outfitted with a Particle Boron microcontroller to enable cellular connectivity and thus mobility. The device sends a query to a Node-Red server (hosted on DigitalOcean) via MQTT. The Node-Red flow, in turn, queries the TomTom API to return the coordinates of the closest Pizza parlor.
Ultimately, the Pizza Finder is able to find the closest Pizza establishment, providing coordinates, distance, and heading.
PizzaFinder.ino
C/C++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;
}
#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_
[
{
"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": ""
}
]
Comments