Project tutorial

Bike Route Data Gatherer

Understanding how people move leads to better infrastructure and services. This device will gather data to make more informed decisions.

  • 8,744 views
  • 0 comments
  • 34 respects

Components and supplies

Apps and online services

About this project

Understanding Pain Points

Over the last decade, Colorado (and the Denver area in particular) has seen huge changes as the population has boomed. As more people end up in the area, more infrastructure needs to be added to support them. One of the areas that needs expanding and upkeep is bike paths, as more people are moving away from car ownership. In addition, self driving cars are on pace to fundamentally change how people get around or even need a car for the majority of their travel.

While building infrastructure is one thing, understanding how it needs to grow in order to do it efficiently is an entirely different problem. In addition to this problem, there's continued interest in cities around the Denver area for bike sharing companies to expand their services.

To have a better idea of what already exists, we can look at the data provided by the State of Colorado and the Department of Transportation. Current bike routes in the city of Denver are mainly on-street riding alongside or in the middle of traffic, as designated by the purple and pink lines in this graphical representation of already existing bike routes around downtown.

Whereas in the suburbs, such as the town of Broomfield, where I live, most of the paths are in open spaces and parks, as shown by green paths. While this is great for recreational uses, these paths don't get many people to businesses or more practical locations.

There's currently one major company that handles bike rentals in the Denver area: BCycle, though others are trying to get into the market. The docking stations for bike rentals are mostly clustered in the downtown areas of Denver and Boulder, with some recently being added in the Broomfield area along highway 36 (near the Rocky Mountain Metropolitan Airport on this map)

Here we can see the huge disconnect in bike routes and paths available in the metro area compared to the services of bike rentals being provided.

In order to help with both the problem of improved bike routes/paths and expansion of bike rental facilities, I put together a prototype for a device that can track a rental bike's location as it is used, and then uploads that data to a datastore, which can then be used to compare routes taken by the user with routes that already exist. I figure the Helium Element would be located inside of the docking station, so when a bike is rented/returned, the state of the device can be updated. As more data is added, it can be analyzed to understand where changes can be efficiently made to improve people's lives or allow businesses to provide practical services.

This project consists of the following parts:

  • Bike route tracking device
  • Helium network
  • Google Cloud IoT Core + Cloud Pub/Sub
  • Google Firebase Functions
  • Data storage (current Firebase, though can be migrated to other cloud options)
  • Data Visualization

Co-Making the Future 2018 Contest Summary Video

Assembling the Tracker Hardware

The tracking device prototype that could be attached to a rental bike consists of

  • an Arduino Mega 2560 (originally I had used an Uno, but quickly ran out of memory. On the plus side, I updated the official library so others can easily use the Mega :))
  • a GPS module
  • a micro-SD card adapter (with SD card)
  • Helium Arduino shield with attached Atom component

The Helium expansion board attaches directly over the Arduino Mega and has a set of pins along the top with jumper connectors. Move the jumper connectors so that the bottom and middle pins on 11 are connected, and the top and middle pins of 10 are connected.

Next, connect the SD card reader to your board using the SPI pins on the Arduino Mega

  • MOSI -> 51
  • MISO -> 50
  • SCK -> 52

For this project, you will also need to connect the clock select (CS) pin to the Arduino pin 4, the VIN connection to 5v and GND to the board's GND.

After your SD card is attached, you will want to add your GPS module. The module will use the Arduino Mega's Serial1 connection.

  • 3.3v VIN -> Arduino 3.3v
  • Rx -> Tx1 (pin 18)
  • Tx -> Rx1 (pin 19)
  • GND -> GND

At this point your tracker should be fully assembled. At the time of this writing I just have the device exposed, though I plan to design a quick 3D printable case for the prototype at a later time.

Setting up Google Cloud IoT Core and Pub/Sub

The majority of the backend for this project is driven by the Google Cloud Platform. To access this, go to the developer console and create a new project.

Once your project is created and you have gone into it, you will need to enter pub/sub into the search bar at the top of the screen to go into the API and enable it.

Once the API is enabled, you will be prompted to create a new Pub/Sub topic.

For this project, I have one simply called location.

After setting up Pub/Sub, you will want to search for IoT Core to enable that API.

Once the API is enabled, you will be prompted to create a new device registry.

The next page will allow you to enter a name for your device registry, select a hosting region, and associate a Pub/Sub topic with your new registry.

After your registry is setup, you will need to create a service account for Helium to communicate directly with IoT Core. You can do this by going into the side navigation drawer on the Google Cloud Platform and selecting IAM & admin, then selecting service accounts.

On the next screen you can create a new service account with the role of Cloud IoT Editor. You will also need to generate a JSON private key that will be used by Helium to connect to Google Cloud IoT Core.

After clicking on CREATE, the JSON file will be generated and saved to your machine. At this point we have what we need to set up a Helium device and connect it to Google IoT Core, though we will return to our Google backend later when we add Firebase Functions support.

Setting up the Helium Network

Helium is a product that allows your IoT devices to connect to networking equipment over fairly long ranges, then handles the routing of uploaded data to various cloud services (in our case, Google IoT Core). There are two main parts: the Element, which is essentially a router that your devices can connect to in order to get to the Internet, and the Atom, which is a specialized transmitter/receiver that your IoT devices can use to communicate with an Element. For this project I used an ethernet connected Element, though cellular elements are available.

To set up your Helium products, you will need to make an account at their site and use their dashboard. At the top of the dashboard you will see two buttons: Add Element and Add Atom

You will select both of these and register your Helium devices using the codes available on their stickers.

After your devices are activated, you will need to create a new channel to act as a middleman between your IoT device and Google IoT Core. Select channels from the side navigation menu, and then select Google Cloud IoT Core from the channels options.

You will then be presented with a screen that asks for your registry ID, region and JSON key. The JSON key is the contents of the JSON file you downloaded in the previous section, the registry ID is what you named your IoT Core registry (in this sample case, helium-smart-bike) and the region is whatever you selected when creating your registry (us-central1 in this case)

After you have filled out the above information, a separate section will appear that will allow you to name your new channel before clicking on the blue Next button.

On the next screen will be able to see some sample code for a "Hello World" sample that connects to your channel. After reviewing the other details on that screen, you can click on the Done button and be taken to your channel's details screen.

Programming the Tracker

Now that Helium and Google Cloud are set up, it's time to program our Arduino IoT device. There are a few libraries that I used that you will need to download and include in your project directory. The first is the Helium Arduino library, and the second is Adafruit's GPS library. With those libraries setup, create a new Arduino project and set the platform at Arduino Mega 2560 and your USB port as the port used by your board.

The first thing you will need to do in your new Arduino file is include all of the dependencies. These include the Helium and GPS libraries, as well as SPI and SD libraries for your SD card reader.

#include "Arduino.h"
#include "Board.h"
#include "Helium.h"
#include <Adafruit_GPS.h>
#include <SPI.h>
#include <SD.h>

Under the #include statements, we define the channel name that Helium uses, a key for device state, the CS pin that we will use for our SD card and the states that our device can be in (idling, collecting GPS data, and uploading that data).

#define CHANNEL_NAME "Google IoT Core"
#define CONFIG_STATE_KEY "channel.state"
#define CS_PIN 4
#define STATE_IDLING 0
#define STATE_COLLECTING 1
#define STATE_UPLOAD 2

After defining your constants, you will need to create a few objects that will be used in this program. The Helium object is a reference to the hardware Atom, Channel is the communication channel back to the Helium network, Config is used for retrieving device state from IoT Core and GPS is our GPS module. This project also use integers to keep track of device state and a timer so that operations only occur every few seconds, rather than multiple times a second.

Helium  helium(&atom_serial);
Channel channel(&helium);
Config config(&channel);
Adafruit_GPS GPS(&Serial1);
int32_t state = 0;
uint32_t timer = millis();

We'll also use a few of the stock utility methods that were provided in the Helium demo code, as they make debugging a little easier.

void report_status(int status)
{
   if (helium_status_OK == status)
   {
       Serial.println("Succeeded");
   }
   else
   {
       Serial.println("Failed");
   }
}
void report_status_result(int status, int result)
{
   if (helium_status_OK == status)
   {
       if (result == 0)
       {
           Serial.println("Succeeded");
       }
       else {
           Serial.print("Failed - ");
           Serial.println(result);
       }
   }
   else
   {
       Serial.println("Failed");
   }
}

Now it's time to initialize hardware. You can initialize your SD card with the following method

void initSDCard() {
 pinMode(CS_PIN, OUTPUT);
 if (!SD.begin(CS_PIN)) {
   Serial.println("Card failed, or not present");
   // don't do anything more:
 } else {
   Serial.println("card initialized.");   
 }
}

and your GPS unit with this method

void initGPS() {
 GPS.begin(9600);
 GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
 GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);   // 1 Hz update rate
 GPS.sendCommand(PGCMD_ANTENNA);
 delay(1000);
}

Before you can use the Helium network to send or receive data, you will need to connect to it. You can do that like so

void connectHelium() {
 helium.begin(115200);
 Serial.print("Connecting - ");
 int status = helium.connect();
 report_status(status);
 int8_t result;
 Serial.print("Creating Channel - ");
 status = channel.begin(CHANNEL_NAME, &result);
 report_status_result(status, result);
}

In order to update device state, the Arduino device will need to retrieve a key from the Helium network off of the channel using the following code

void update_config()
{
   Serial.println(F("Fetching Config - "));
   int status = config.get(CONFIG_STATE_KEY, &state, 2);
   Serial.println(state, DEC);
}

When GPS data is available, you will need to read it from the GPS module and store it on the SD card. It is stored on the SD card here so that all of the data can be uploaded when a Helium Element is nearby and available.

void logGPS() {
 if( GPS.fix ) {
     File dataFile = SD.open("gps.txt", FILE_WRITE);
     if (dataFile) {
       dataFile.print(GPS.latitudeDegrees, 4);
       dataFile.print(",");
       dataFile.println(GPS.longitudeDegrees, 4);;
       dataFile.close();
     }  
     Serial.print(GPS.latitudeDegrees, 4);
     Serial.print(","); 
     Serial.println(GPS.longitudeDegrees, 4);
 } else {
     Serial.println("No fix");
 }
}

When the device switches to an upload state, you will need to send data from your IoT device to the Helium network. For our particular case, we will read data from the SD card, parse it into a JSON format, and then upload it. After the upload has been attempted, the SD card will be cleared for the next trip.

void sendData() {
   File dataFile = SD.open("gps.txt", FILE_READ);
    if (dataFile) {
       char line[40];
       while( dataFile.available() ) {
         readLine(dataFile, line, sizeof(line));
         String lat = getValue(line, ',', 0);
         String lng = getValue(line, ',', 1);
         //TODO Group these together. Max 32kb
         String formattedString = "{\"lat\":" + lat + ",\"lng\":" + lng + "}";
         Serial.println(formattedString);
         char data[50];
         formattedString.toCharArray(data, 50);
         int8_t result;
         channel.send(data, strlen(data), &result);
         delay(200);
       }
    }
    SD.remove("gps.txt");
}

One improvement I would like to make here is combining all of the route data into as few sends as possible. The delay was required because Helium would post an error if I sent too many requests too quickly. I also don't know the nuances of C well enough to manipulate strings/char* efficiently, but I can fix that later ¯\_(ツ)_/¯. For now things work and demonstrate how to send data, and I'm happy with that. You may notice that there's a method in there called readLine(). This is used to parse the file, and is a modified version of something I found on an Arduino forum post. The methods used are defined as

bool readLine(File &f, char* line, size_t maxLen) {
 for (size_t n = 0; n < maxLen; n++) {
   int c = f.read();
   if ( c < 0 && n == 0) return false;  // EOF
   if (c < 0 || c == '\n') {
     line[n] = 0;
     return true;
   }
   line[n] = c;
 }
 return false; // line too long
}
String getValue(String data, char separator, int index)
{
   int found = 0;
   int strIndex[] = { 0, -1 };
   int maxIndex = data.length() - 1;
   for (int i = 0; i <= maxIndex && found <= index; i++) {
       if (data.charAt(i) == separator || i == maxIndex) {
           found++;
           strIndex[0] = strIndex[1] + 1;
           strIndex[1] = (i == maxIndex) ? i+1 : i;
       }
   }
   return found > index ? data.substring(strIndex[0], strIndex[1]) : "";
} 

Now that all of the helper methods are done, you can initialize the device in the setup() method and check for any new configuration data

void setup()  
{
 Serial.begin(19200);
 connectHelium();
 initSDCard();
 initGPS();
 update_config();
}

and in your loop() method, check the timer, see if GPS data is working, and handle the current device state before checking for any configuration updates.

void loop()
{ 
 // if millis() or timer wraps around, we'll just reset it
 if (timer > millis())  timer = millis();
 char c = GPS.read();
 // if a sentence is received, we can check the checksum, parse it...
 if (GPS.newNMEAreceived()) {
   if (!GPS.parse(GPS.lastNMEA())) 
     return;
 }
 if (millis() - timer > 5000) { 
   timer = millis(); // reset the timer
   switch( state ) {
     case STATE_IDLING:
       break;
     case STATE_COLLECTING:
       logGPS();
       break;
     case STATE_UPLOAD:
       sendData();
       break;
     default:
       Serial.println("Unsupported State");
   }
   update_config();
 }
}

Configuring Firebase for Data Processing/Storage

Once data is flowing from your IoT device into Google Cloud IoT Core, it's time to do something with it. For this project I created a Firebase portion to the Google Cloud project so that a Firebase Function can listen for Pub/Sub events, then store the device's data in a Firebase Database. To start, go to the Firebase Console and create a new Firebase project. Be sure to select your Google Cloud project from the dropdown menu.

After your project is created, you will want to select the database option from the side navigation bar and create a new Realtime Database

Once your database is started, you can go under the Rules tab and disable authentication for testing purposes. You will not want to do this with a production app, it just helps for debugging while building a prototype.

After your database is set up, it's time to add a Firebase Function. I'm using OSX as my computer OS with Homebrew installed, so depending on your setup, you may need to do some things in this section of the tutorial a little differently. Create a new directory location to store your local environment, and navigate to it in your terminal.

After navigating to your new directory, ensure that you have NodeJS installed on your computer. I did this with the command

brew install npm

Once you know that Node is installed, you can run the following command to install the Firebase tools.

npm install -g firebase-tools

With the tools installed, you can run the command firebase login, which will prompt you through a browser window to authenticate against your Google account.

After authenticating, run the command firebase init functions to initialize your local environment for Firebase Functions

After selecting your project, you will be prompted to select a programming language for our Cloud Functions. For this project I went with JavaScript. I also did not elect to use ESLint, and did install the additional dependencies.

When that finishes, you can go into the directory that you created for your Firebase Functions and open the index.js file, which is where you will add your new code.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const db = admin.database();
exports.receiveTelemetry = functions.pubsub
 .topic('location')
 .onPublish(event => {
   const attributes = event.data.attributes;
   const message = event.data.json;
   const deviceId = event.data.attributes.deviceId;
   const data = {
     lat: message.lat,
     lng: message.lng
   };
   return Promise.all([
     updateCurrentDataFirebase(data, deviceId)
   ]);
 });
function updateCurrentDataFirebase(data, deviceId) {
 var d = new Date();
 var timeInMillis = d.getTime();
 return db.ref(`/devices/${deviceId}/${timeInMillis}`).set({
   lat: data.lat,
   lng: data.lng
 });
}

The above snippet will listen for the telemetry event from Pub/Sub, parse out the latitude and longitude that were sent from the Arduino, and store them into Firebase under the device ID and the received time. The base of this code came from a sample/tutorial on Firebase Functions by Alvaro Viebrantz, though I modified it to fit our purposes.

After writing your new Firebase Function, you can upload it to Firebase with the firebase deploy --only functions command

Testing Everything Out

The IoT device that we are using is state driven. In state 0, the device idles, in state 1, GPS data is collected and stored in a text file, and in state 2 the data is uploaded to Google Cloud IoT Core. Right now the state changing process is manual, though I will want to look into automating that as I learn more about IoT Core. To manually change the state, you will need to go into the IoT Core console, select your registry, and select the device that you would like to change. From there, you will see an UPDATE CONFIG button at the top of the screen.

Select that and a new menu will appear. You can paste in your configuration data there in JSON format. To place our device into an idling state, you would enter this data as text.

{
"state":0
}

As we run our device and view the serial monitor, we will see that the device is in an idling state.

Connecting - Succeeded
Creating Channel - Succeeded
card initialized.
Fetching Config - 
0

If we switch the state to 1 via the IoT Core console, we will see the following

Fetching Config - 
1
39.9256, -105.0294
Fetching Config - 
1
39.9256, -105.0294
Fetching Config - 
1
39.9256, -105.0294
Fetching Config - 
1
39.9256, -105.0294

If we switch this to state 2, we should see the data upload to Helium, and then into Firebase

Fetching Config - 
2
{"lat":39.9256,"lng":-105.0294}
{"lat":39.9256,"lng":-105.0294}
{"lat":39.9256,"lng":-105.0294}

At this point I would want to update the Firebase Function to somehow change the state to 0, until we establish that we want to re-start logging the GPS. However, since the SD card was cleared after uploading, this prototype idles for all intents and purposes.

Trying in the Real World

Now that we've seen how the sample works while sitting in one place, I want to show how the data looks if I go for quick walk around the block.

After returning and changing the device state from collecting to uploading, a set of entries show up in Firebase matching up to my walk.

When we parse through these items and put the data points into a standard coordinate mapper, we can get something like this

On a bike the corners would be a little more iffy, but that's OK. You can see a cut corner where I turned on the sidewalk and it just connected the two points together, but it caught the corner where I went to check my mail while out (this, subsequently, turned out to be multiple point data points as I was in one place).

Now that data can be gathered and stored, there's a lot more interesting things that can be done to compare routes and other pre-existing data to solve more complex data problems.

Room for Improvement

As with any project, there's always going to be room for improvement. For this project, I would like to programmatically change state for individual devices, rather than abstracting it away for this demo (Update, done!). There's also some weirdness that I worked around with the GPS module where it wasn't getting a fix if I didn't check for a new signal every loop, and would also occur if I used delays in the main loop. This is probably more of an issue with the GPS library I used, but it did well enough for my purposes. I'd also like to more efficiently send data since a production app would quickly out grow Google's free tier. Finally, I want to design and print a case for the project. Not sure when/if this will really happen, but it's on the radar.

Final Thoughts

Overall this project was a lot more straight forward than I was expecting. I had the idea in my head for a while, but was going to do something with making my own gateway and using LoWPAN devices with Android Things. After I found out about Helium and how it worked with Google Cloud IoT Core, I figured I'd give it a shot. It honestly abstracted enough away that I was able to toss this project together in about a day of reading and figuring out the backend stuff, and I'm pretty happy with how it turned out for a quick prototype/learning project. Hopefully it helps others build some cool stuff. Enjoy!

Updates Since Original Posting

Under the section Room for Improvement, I listed a few things that I'd like to come back to and update. Not sure how much time I'll sink into optimization, but I definitely wanted to programmatically send data down to the Helium IoT device, and get a case 3D printed. Rather than editing the original post, I figured I'd write an addendum so people can see the process that goes into building a project, rather than making it look like everything just worked out from the beginning :p

Programmatically Updating Device State - Firebase

Earlier in this project I described how to manually change the state of a device through the Cloud IoT Core console. If this were to be a real project, that state would need to change programmatically. Rather than just leaving the project as it was, I decided to figure out how to change device state from another Firebase Function, which I then trigger through an Android app (though web, iOS or even another IoT device could similarly access it). In order for the Firebase Function to change the device state in Cloud IoT Core, you will need to create a new service account (just like you did for Helium) and save that JSON file in your Firebase Functions directory next to your index.js file.

Once you have your JSON file, you will need to add new methods to your index.js file to update the device state. Include the following dependencies at the top of your file.

const googleapis = require('googleapis');
const fs = require('fs');

You will also need additional constants related to the Cloud IoT Core API

const API_SCOPES = 'https://www.googleapis.com/auth/cloud-platform';
const API_VERSION = 'v1';
const DISCOVERY_API = 'https://cloudiot.googleapis.com/$discovery/rest';
const SERVICE_NAME = 'cloudiot';
const DISCOVERY_URL = `${DISCOVERY_API}?version=${API_VERSION}`;

While we could send in data related to our device, I have hard coded the device since I only have one Atom module and I wanted to keep the process relatively simple while still demonstrating how to send data from an app to the IoT device. In addition to the deviceId value, you will need your project id, cloud region and registry id.

const projectId = "your-project-id-here";
const cloudRegion = "us-central1";
const registryId = "your-project-registry-id-here";
const deviceId = "your-device-id-here";//Can be retrieved from device if you want

Once you have the first three constants in the above snippet, you can define paths and a version for your project.

const version = 0;
const parentName = `projects/${projectId}/locations/${cloudRegion}`;
const registryName = `${parentName}/registries/${registryId}`;

Next you will need to write a function that will authenticate against Google using the service account JSON that you generated earlier.

function getClient() {
 return new Promise((resolve, reject) => {
   const serviceAccount = JSON.parse(fs.readFileSync('SmartBike-b0112e161cdf.json'));
   const jwtAccess = new googleapis.auth.JWT();
   jwtAccess.fromJSON(serviceAccount);
   jwtAccess.scopes = API_SCOPES;
   googleapis.options({auth: jwtAccess});
   const discoveryUrl = `${DISCOVERY_API}?version=${API_VERSION}`;
   googleapis.discoverAPI(discoveryUrl, {}, (err, client) => {
     if (err) {
       console.log('Error during API discovery', err);
       return reject(err);
     }
     return resolve(client);
   });
 });
}

After putting together your method to retrieve the authenticated client, you will need a method that can accept the client and the data sent from another client, parse it and update the device via a request. The first thing this method does is convert the data received from an object to a map (we'll go over how this data is sent to the function later, but for know, just know it's already being sent to us as a map).

function sendConfigToDevice(client, data) {
 const myMap = new Map(
     Object
         .keys(data)
         .map(
             key => [key, data[key]]
         )
 )
 console.log("data: " + myMap.get("state"));

With the map created, you will need to retrieve the state data from the map, create your state JSON string, convert it to binary data and create the request that will be sent to Google Cloud IoT Core.

 const newData = "{ \"state\": " + myMap.get("state") + "}";
 const binaryData = new Buffer(newData, 'utf-8').toString('base64');
 const request = {
   name: `${registryName}/devices/${deviceId}`,
   versionToUpdate: version,
   binaryData: binaryData
 };

Now that the request is created, you will need to send it. We will create a new Promise method to handle sending the data with the modifyCloudToDeviceConfig call and processing the result, then send that back to the entry-point method.

 return new Promise((resolve, reject) => {
     client.projects.locations.registries.devices.modifyCloudToDeviceConfig(request,
       (err, response) => {
         if (!err) {
           console.log("should modify cloud config");
           resolve(response);
           return;
         }
         console.log("reject on modifyCloudToDeviceConfig");
         reject(err);
       }
     );
   }); 
 }

At this point the bulk of the Firebase update is done. We will need to create one additional method that can be called by an outside app. This new method will call our authentication method and pass received data to sendConfigToDevice.

exports.updateDeviceState = functions.https.onCall((data) => {
 getClient()
     .then(client => {
       console.log("before send config to device");
       return sendConfigToDevice(client, data);
     })
     .then(response => {
       console.log('SendConfigToDevice:', response);
     })
     .catch(err => {
         console.log('Exception catching:', err);
       });
});

Programmatically Updating Device State - Android

Now that the Firebase Functions portion is complete, we will need an additional app to trigger the function. Since I typically do Android development, I decided to make an Android app that uses a new feature in Google Play Services 12.0.0 that allows apps to directly call Firebase Functions. If you don't already have Android Studio installed, you will need to start by following these directions.

With Android Studio installed on your machine, you will need to create a mobile app by going through the new app wizard. Screenshots of the process are included below.

Next, return to the Firebase console and add your app. This process will generate a JSON file that will need to be included in your Android app, and you will need to update both build.gradle files to include Firebase functionality.

While in your module level build.gradle file, add the following line to the dependencies node to use Firebase Functions from your Android app.

compile 'com.google.firebase:firebase-functions:12.0.0'

Now that the setup for Firebase is done, open the activity_main.xml file and add the following for a simple (and I mean no frills whatsoever :p) layout with three buttons.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/idle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Set State to Idling"
        android:onClick="idlingState"/>
    <Button
        android:id="@+id/log"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Set State to Logging"
        android:onClick="collectingState"/>
    <Button
        android:id="@+id/upload"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Set State to Uploading"
        android:onClick="uploadingState"/>
</LinearLayout>

Each of these buttons will call a specific method in MainActivity.java, which will in turn update the device config.

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    private void updateDeviceState(int state) {
    }
    public void idlingState(View v) {
        updateDeviceState(0);
    }
    public void collectingState(View v) {
        updateDeviceState(1);
    }
    public void uploadingState(View v) {
        updateDeviceState(2);
    }
}

The final thing we will need to do is change updateDeviceState() to create a map and send data to the Firebase Function that we created in the last section. This involves creating a new String key/object map, getting an instance of the FirebaseFunctions object and naming the method that the data should be sent to.

private void updateDeviceState(int state) {
    Map<String, Object> data = new HashMap<>();
    data.put("state", state);
    FirebaseFunctions.getInstance()
            .getHttpsCallable("updateDeviceState")
            .call(data);
}

When this method is called, it will pass the new map to our Firebase Function, which will in turn update our IoT device's state configuration. You can see the logs and process from the Firebase console, like so.

If we go into the Google Cloud IoT Core console, we can also see the changed config under our device.

Migrating to Firebase Functions v1.0

The same day that I added the updated firebase functions code, Google took it out of beta. Let's take a look at what needed to change to get up and running on the official release!

First you will need to update your dependencies for firebase-admin and firebase-functions from a command line in your project directory.

npm install firebase-functions@latest --save

Luckily, there were no changes that needed to be made for authenticating and updating the device state. This was not the case for updating the Firebase database. When initializing firebase-admin, you will need to remove the parameters in the initializeApp() method.

admin.initializeApp();

I also switched const admin and const db and to be a vars. I then changed the return statement for the set method to be broken into a separate ref var and called the method on that.

function updateCurrentDataFirebase(data, deviceId) {
 var d = new Date();
 var timeInMillis = d.getTime();
 var ref = db.ref(`/devices/${deviceId}/${timeInMillis}`);
 return ref.set({
   lat: data.lat,
   lng: data.lng
 });
}

Finally, the pubsub listener method no longer accepts an event, but rather accepts the data object that we originally extracted from the event.

exports.receiveTelemetry = functions.pubsub
 .topic('telemetry-topic')
 .onPublish((data) => {
   const attributes = data.attributes;
   const message = data.json;
   const deviceId = attributes.deviceId;

Overall the changes weren't too major. There were a couple things for me to figure out since I'm not super familiar with Firebase Functions or Node.JS, but I got through it quickly enough going through the official migration guide and the admin Firebase docs. I've included the new code at the bottom of this project alongside the old code, so folks can work on the project with whatever dependencies they need.

Visualizing Data

We have data stored in Firebase, but what good is data if we're not actually using it for something? This section is going to be a bit messy, as I have almost no experience with web development or python, but I wanted to try and make a quick Google Map that displays bike routes from an official Denver data source, and a heat map with our new data. Messy is half the fun, and just means the next time we need to use something similar, it'll be that much better :p

First we need to get the bike route infrastructure data that was used for a screenshot easier in this project. We can view the original map here, and grab the data files from here. If you look around that site, you'll also find data for major/minor roads, traffic counts at specific points, traffic accident data, and other data that could be useful when analyzed with how people are moving. I ended up getting the KML file, as I have no idea how to convert GeoJSON coordinates :) This KML file can be opened in the free version of Google Earth, or as a text file in order to see the actual data. If we look at the text file, we can see that there's a LOT of data, most of which we won't even need to use.

To clean this up, I made a series of small Python scripts that handle one clean-up task at a time. These could be grouped together into one script, but while figuring things out I tend to like small modular tasks :) The first script removes any data that isn't wrapped in a color, coordinates or name tag.

import re
f = open("DRCOGPUB-bicycle_facility_inventory.txt", "r")
lines = f.readlines()
f.close()
f = open("trimmed.txt", "w")
regex = re.compile('^<name>|^<color>|^<coordinates>')
for line in lines:
	if re.match(regex, line.lstrip()):
		f.write(line)

Which causes our new data to look like this

You'll notice there's more than one color item per path. The 00ffffff colors aren't necessary for our map display, so let's remove those next.

import re
f = open("trimmed.txt", "r")
lines = f.readlines()
f.close()
f = open("trimmed2.txt", "w")
regex = re.compile('^<color>00ffffff</color>')
for line in lines:
	if not re.match(regex, line.lstrip()):
		f.write(line)

Now we're looking a bit better, though we still have an extra coordinates tag that was originally used to zoom in on a specific path. We can remove that easily enough since there's only ever one point in that coordinates section, and every actual path has more than one point (hence being a path :))

import re
f = open("trimmed2.txt", "r")
lines = f.readlines()
f.close()
f = open("trimmed3.txt", "w")
regex = re.compile('^<coordinates>')
regex2 = re.compile('^<name>|^<color>')
for line in lines:
	if (re.match(regex, line.lstrip()) and ' ' in line.lstrip()) or re.match(regex2, line.lstrip()):
		f.write(line.lstrip())

Now we're getting a lot closer. There's two last things that I want to do - separate the coordinates out to their own lines, and then swap them so they're in a latitude, longitude ordering.

import re
f = open("trimmed2.txt", "r")
lines = f.readlines()
f.close()
f = open("trimmed3.txt", "w")
regex = re.compile('^<coordinates>')
regex2 = re.compile('^<name>|^<color>')
for line in lines:
	if (re.match(regex, line.lstrip()) and ' ' in line.lstrip()) or re.match(regex2, line.lstrip()):
		f.write(line.lstrip())

and

import re
f = open("trimmed4.txt", "r")
lines = f.readlines()
f.close()
f = open("trimmed5.txt", "w")
regex = re.compile('^-')
for line in lines:
	if re.match(regex, line):
		lng,lat = line.split(",")
		lng = lng.replace('\n', '')
		lat = lat.replace('\n', '')
		f.write(lat + ',' + lng)
		f.write('\n')
	else:
		f.write(line)

After all of this roundabout (and totally inefficient, though surprisingly fast - mmm Python) messing with data, we get this

This is where things get a bit messier. I don't know JavaScript super well or how to read text files locally easily, so I just threw the final text file up on Firebase Storage so I could use JavaScript's handy fetch operation.

After uploading your file, select it and make note of the download link. You will need this later.

In order to access this file, we'll need to first update our authentication rules in Firebase Storage so a user does not need to be authenticated to access files. This is definitely not the way to handle a production/more serious app, but we're just hacking here, so let's just make things work and worry about security issues later if this ever becomes a real product (hey bike share companies and investors, I'm open :p)

We will also want to configure CORS so we can download the file from Firebase Storage. Follow the directions for installing gsutil here.

After initializing gsutil, create a new JSON file named cors.json and add the following contents

[
  {
    "origin": ["*"],
    "method": ["GET"],
    "maxAgeSeconds": 3600
  }
]

You'll then upload the new file with this command

gsutil cors set cors.json gs://<your-cloud-storage-bucket>

While we're getting set up, go to the Google Cloud Console, search for maps, and select the Google Maps JavaScript API option.

After going to the next page, select the blue ENABLE button and wait for the API to turn on. After that's ready, go to navigation column on the console and select credentials. You should see an auto-created API key for browsers in the row that starts with Browser key (auto created by Google Service). You will need this for connecting your JavaScript page to Google Maps.

Now that things are set up, create a new HTML file that will show our map. Mine is named map.html. The base content of the file will look like this

<!DOCTYPE html>
<html>
 <head>
   <title>Route</title>
   <meta name="viewport" content="initial-scale=1.0">
   <meta charset="utf-8">
   <style>
     /* Always set the map height explicitly to define the size of the div
      * element that contains the map. */
     #map {
       height: 100%;
     }
     /* Optional: Makes the sample page fill the window. */
     html, body {
       height: 100%;
       margin: 0;
       padding: 0;
     }
   </style>
 </head>
 <body>
 </script>
   <script src="https://maps.googleapis.com/maps/api/js?key=<YOUR-API-KEY-HERE>&libraries=visualization&callback=initMap"
   async defer></script>
 </body>
</html>

where <YOUR-API-KEY-HERE> in the script tag is the API key that you saved earlier. The script tag will initialize Google Maps, a heat map library and call initMap once maps are ready.

Inside of the <body> block, we will create a variable named map, as well as add the initMap function, which will set up the map, retrieve our file, parse it and add the paths.

var map;
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
 center: {lat: 40.01972, lng: -105.2764},
 zoom: 18
});
fetch('https://firebasestorage.googleapis.com/v0/b/filepath')
.then(response => response.text())
.then(text => {
 lines = text.split('\n');
 var pathData = []
 var color;
 for( var i = 1; i < lines.length; i++ ) {
   if( lines[i].includes("<name>") ) {
     var path = new google.maps.Polyline({
       path: pathData,
       geodesic: true,
       strokeColor: color,
       strokeOpacity: 1.0,
       strokeWeight: 4
     });
     pathData = [];
     path.setMap(map);
   } else if( lines[i].includes("color") ) {
       color = "#" + lines[i].substring(9, 15);
   } else if( !lines[i].includes("coordinates") ) {
       var splitData = lines[i].split(",", 2);
       pathData.push({ lat: parseFloat(splitData[0]), lng: parseFloat(splitData[1])});
   }
 }
})
}

This isn't the prettiest parsing, but it works, and I'm OK with that for now :p After saving and opening this file in your browser, you should see something similar to this after zooming out a little bit

Now let's add our own data. Go back into the Firebase Console and add a new app. This time it'll be a web app, which will walk you through getting a configuration code block to add to your web file. I pasted this snippet into the <head> section of my HTML document.

<script src="https://www.gstatic.com/firebasejs/4.12.1/firebase.js"></script>
<script>
 // Initialize Firebase
 var config = {
   apiKey: "API_KEY",
   authDomain: "FIREBASE_PROJECT",
   databaseURL: "https://BUCKET.firebaseio.com",
   projectId: "helium-smart-bike",
   storageBucket: "helium-smart-bike.appspot.com",
   messagingSenderId: "5558675309"
 };
 firebase.initializeApp(config);
</script>

Returning to the initMap() function, I retrieve a reference back to my Helium device (I only have one, otherwise I'd loop through them or do something fancier for separate routes - that'd all require a different structuring to the data beyond what I currently need, though) and then loop through all of the saved lat/lngs for that device to create a heat map.

var database = firebase.database();
       var locations = firebase.database().ref('devices/Helium-6081f9fffe0020be');
       locations.on('value', function(snapshot) {
         var path1HeatmapData = [];
         snapshot.forEach(function(childSnapshot) {
           var childData = childSnapshot.val();
           path1HeatmapData.push(new google.maps.LatLng(childData.lat, childData.lng));
         });
         var path1Heatmap = new google.maps.visualization.HeatmapLayer({
           map: map,
           data: path1HeatmapData,
           radius: 20,
           opacity: 1.0
       });
       });

At this point we should see the points that were collected by the device as a heat map along with the Denver area's bike infrastructure all on a single map. While working on this project some more, I went and collected some data along a street in downtown Boulder, as you can see here.

And because I love it: one of the fun things you get out of the box with Google Maps is street view, so you can see certain points along a route, plus markers if you have some sort of algorithm for picking out the best locations.

3D Printed Case

It took me a bit, but I finally came around to 3D printing a case after modifying another that I found on Thingiverse :) The top and bottom are connected with four long 3M screws, and the electronics are fastened to the bottom of the case with six smaller 3M screws. I've attached the STL files to this tutorial.

The next thing I want to look into for this project, though I'm not sure when, would be a better power supply with a portable solar panel that I recently picked up from SparkFun.

Code

Firebase FunctionJavaScript
Firebase function for receiving a Pub/Sub, parsing data sent from the device and storing it in Firebase
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const googleapis = require('googleapis');
const fs = require('fs');

const API_SCOPES = 'https://www.googleapis.com/auth/cloud-platform';
const API_VERSION = 'v1';
const DISCOVERY_API = 'https://cloudiot.googleapis.com/$discovery/rest';
const SERVICE_NAME = 'cloudiot';
const DISCOVERY_URL = `${DISCOVERY_API}?version=${API_VERSION}`;

const projectId = "";
const cloudRegion = "us-central1";
const registryId = "";
const deviceId = "";

const version = 0;
const parentName = `projects/${projectId}/locations/${cloudRegion}`;
const registryName = `${parentName}/registries/${registryId}`;

admin.initializeApp(functions.config().firebase);
const db = admin.database();

exports.receiveTelemetry = functions.pubsub
  .topic('telemetry-topic')
  .onPublish(event => {
  	const attributes = event.data.attributes;
    const message = event.data.json;
    const deviceId = event.data.attributes.deviceId;
  	
    const data = {
      lat: message.lat,
      lng: message.lng
    };

    return Promise.all([
      updateCurrentDataFirebase(data, deviceId)
    ]);
  });

function updateCurrentDataFirebase(data, deviceId) {
    var d = new Date();
	var timeInMillis = d.getTime();
  return db.ref(`/devices/${deviceId}/${timeInMillis}`).set({
    lat: data.lat,
    lng: data.lng
  });
}

exports.updateDeviceState = functions.https.onCall((data) => {
  getClient()
      .then(client => {
        console.log("before send config to device");
        return sendConfigToDevice(client, data);
      })
      .then(response => {
        console.log('SendConfigToDevice:', response);
      })
      .catch(err => {
          console.log('Exception catching:', err);
        });
});

function sendConfigToDevice(client, data) {
  const myMap = new Map(
      Object
          .keys(data)
          .map(
              key => [key, data[key]]
          )
  )

  console.log("data: " + myMap.get("state"));

  const newData = "{ \"state\": " + myMap.get("state") + "}";
  const binaryData = new Buffer(newData, 'utf-8').toString('base64');

  const request = {
    name: `${registryName}/devices/${deviceId}`,
    versionToUpdate: version,
    binaryData: binaryData
  };


  return new Promise((resolve, reject) => {
      client.projects.locations.registries.devices.modifyCloudToDeviceConfig(request,
        (err, response) => {
          if (!err) {
            console.log("should modify cloud config");
            resolve(response);
            return;
          }
          console.log("reject on modifyCloudToDeviceConfig");
          reject(err);
        }
      );
    }); 
  }

function getClient() {
  return new Promise((resolve, reject) => {
    const serviceAccount = JSON.parse(fs.readFileSync('SmartBike.json'));
    const jwtAccess = new googleapis.auth.JWT();
    jwtAccess.fromJSON(serviceAccount);
    jwtAccess.scopes = API_SCOPES;
    googleapis.options({auth: jwtAccess});
    const discoveryUrl = `${DISCOVERY_API}?version=${API_VERSION}`;
    googleapis.discoverAPI(discoveryUrl, {}, (err, client) => {
      if (err) {
        console.log('Error during API discovery', err);
        return reject(err);
      }
      return resolve(client);
    });
  });
}
linedeleter.pyPython
import re

f = open("DRCOGPUB-bicycle_facility_inventory.txt", "r")

lines = f.readlines()

f.close()

f = open("trimmed.txt", "w")

regex = re.compile('^<name>|^<color>|^<coordinates>')

for line in lines:
	if re.match(regex, line.lstrip()):
		f.write(line)
removeunneededcoordinates.pyPython
import re

f = open("trimmed2.txt", "r")

lines = f.readlines()

f.close()

f = open("trimmed3.txt", "w")

regex = re.compile('^<coordinates>')
regex2 = re.compile('^<name>|^<color>')

for line in lines:
	if (re.match(regex, line.lstrip()) and ' ' in line.lstrip()) or re.match(regex2, line.lstrip()):
		f.write(line.lstrip())
removeunnneededcolors.pyPython
import re

f = open("trimmed.txt", "r")

lines = f.readlines()

f.close()

f = open("trimmed2.txt", "w")

regex = re.compile('^<color>00ffffff</color>')

for line in lines:
	if not re.match(regex, line.lstrip()):
		f.write(line)
swapcoordinates.pyPython
import re

f = open("trimmed4.txt", "r")
lines = f.readlines()
f.close()
f = open("trimmed5.txt", "w")
regex = re.compile('^-')

for line in lines:
	if re.match(regex, line):
		lng,lat = line.split(",")
		lng = lng.replace('\n', '')
		lat = lat.replace('\n', '')
		f.write(lat + ',' + lng)
		f.write('\n')
	else:
		f.write(line)
Arduino Code for Helium GPS TrackerC/C++
Code that runs on the Arduino Mega 3560 with a Helium Atom shield, GPS module and SD card adapter
#include "Arduino.h"
#include "Board.h"
#include "Helium.h"
#include <Adafruit_GPS.h>
#include <SPI.h>
#include <SD.h>

#define CHANNEL_NAME "Google IoT Core"
#define CONFIG_STATE_KEY "channel.state"
#define CS_PIN 4
#define STATE_IDLING 0
#define STATE_COLLECTING 1
#define STATE_UPLOAD 2

Helium  helium(&atom_serial);
Channel channel(&helium);
Config config(&channel);
Adafruit_GPS GPS(&Serial1);

int32_t state = 0;
uint32_t timer = millis();

void update_config(bool stale)
{
    Serial.println(F("Fetching Config - "));
    int status = config.get(CONFIG_STATE_KEY, &state, 2);
    Serial.println(state, DEC);
}

void report_status(int status)
{
    if (helium_status_OK == status)
    {
        Serial.println("Succeeded");
    }
    else
    {
        Serial.println("Failed");
    }
}

void report_status_result(int status, int result)
{
    if (helium_status_OK == status)
    {
        if (result == 0)
        {
            Serial.println("Succeeded");
        }
        else {
            Serial.print("Failed - ");
            Serial.println(result);
        }
    }
    else
    {
        Serial.println("Failed");
    }
}

void connectHelium() {
  helium.begin(115200);
  Serial.print("Connecting - ");
  int status = helium.connect();
  report_status(status);
  int8_t result;
  Serial.print("Creating Channel - ");
  status = channel.begin(CHANNEL_NAME, &result);
  report_status_result(status, result);
}

void initSDCard() {
  pinMode(CS_PIN, OUTPUT);
  if (!SD.begin(CS_PIN)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
  } else {
    Serial.println("card initialized.");   
  }
}

void initGPS() {
  GPS.begin(9600);
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ);   // 1 Hz update rate
  GPS.sendCommand(PGCMD_ANTENNA);
  delay(1000);
}

void channel_poll(void * data, size_t len, size_t * used)
{
    int status;
    do
    {
        Serial.print("Polling - ");
        // Poll the channel for some data for some time
        status = channel.poll_data(data, len, used);
    } while (status != helium_status_OK);
}

bool readLine(File &f, char* line, size_t maxLen) {
  for (size_t n = 0; n < maxLen; n++) {
    int c = f.read();
    if ( c < 0 && n == 0) return false;  // EOF
    if (c < 0 || c == '\n') {
      line[n] = 0;
      return true;
    }
    line[n] = c;
  }
  return false; // line too long
}

String getValue(String data, char separator, int index)
{
    int found = 0;
    int strIndex[] = { 0, -1 };
    int maxIndex = data.length() - 1;

    for (int i = 0; i <= maxIndex && found <= index; i++) {
        if (data.charAt(i) == separator || i == maxIndex) {
            found++;
            strIndex[0] = strIndex[1] + 1;
            strIndex[1] = (i == maxIndex) ? i+1 : i;
        }
    }
    return found > index ? data.substring(strIndex[0], strIndex[1]) : "";
}

void sendData() {

    File dataFile = SD.open("gps.txt", FILE_READ);
     if (dataFile) {
        char line[40];
        while( dataFile.available() ) {
          readLine(dataFile, line, sizeof(line));
          String lat = getValue(line, ',', 0);
          String lng = getValue(line, ',', 1);

          //TODO Group these together. Max 32kb
          String formattedString = "{\"lat\":" + lat + ",\"lng\":" + lng + "}";
          Serial.println(formattedString);
          char data[50];
          formattedString.toCharArray(data, 50);
          int8_t result;
          channel.send(data, strlen(data), &result);
          delay(200);
        }
     }

     SD.remove("gps.txt");
}

void setup()  
{
  Serial.begin(19200);
  connectHelium();
  initSDCard();
  initGPS();
  update_config(true);
}

void logGPS() {
  
  if( GPS.fix ) {
      File dataFile = SD.open("gps.txt", FILE_WRITE);
      
      if (dataFile) {
        dataFile.print(GPS.latitudeDegrees, 4);
        dataFile.print(",");
        dataFile.println(GPS.longitudeDegrees, 4);
        dataFile.close();
      }  
      
      Serial.print(GPS.latitudeDegrees, 4);
      Serial.print(", "); 
      Serial.println(GPS.longitudeDegrees, 4);
  } else {
      Serial.println("No fix");
  }
}

void loop()
{ 

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

  char c = GPS.read();

  // if a sentence is received, we can check the checksum
  if (GPS.newNMEAreceived()) {
    if (!GPS.parse(GPS.lastNMEA())) 
      return;
  }

  if (millis() - timer > 5000) { 
    timer = millis(); // reset the timer
    
    switch( state ) {
      case STATE_IDLING:
        break;
      case STATE_COLLECTING:
        logGPS();
        break;
      case STATE_UPLOAD:
        sendData();
        break;
      default:
        Serial.println("Unsupported State");
    }

    update_config(true);
  }
  
    
}
Android Firebase Config Updater MainActivity.javaJava
Java class for calling a Firebase function
package com.ptrprograms.heliumsmartbikecontroller;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

import com.google.firebase.functions.FirebaseFunctions;

import java.util.HashMap;
import java.util.Map;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    private void updateDeviceState(int state) {
        Map<String, Object> data = new HashMap<>();
        data.put("state", state);

        FirebaseFunctions.getInstance()
                .getHttpsCallable("updateDeviceState")
                .call(data);
    }

    public void idlingState(View v) {
        updateDeviceState(0);
    }

    public void collectingState(View v) {
        updateDeviceState(1);
    }

    public void uploadingState(View v) {
        updateDeviceState(2);
    }
}
Layout File for Android State UpdaterXML
Layout file for Android state updater
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/idle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Set State to Idling"
        android:onClick="idlingState"/>

    <Button
        android:id="@+id/log"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Set State to Logging"
        android:onClick="collectingState"/>

    <Button
        android:id="@+id/upload"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Set State to Uploading"
        android:onClick="uploadingState"/>

</LinearLayout>
Firebase Functions v1 CodeJavaScript
Updated code to work with Firebase Functions v1
const functions = require('firebase-functions');
var admin = require('firebase-admin');
const googleapis = require('googleapis');
const fs = require('fs');

const API_SCOPES = 'https://www.googleapis.com/auth/cloud-platform';
const API_VERSION = 'v1';
const DISCOVERY_API = 'https://cloudiot.googleapis.com/$discovery/rest';
const SERVICE_NAME = 'cloudiot';
const DISCOVERY_URL = `${DISCOVERY_API}?version=${API_VERSION}`;

const projectId = "";
const cloudRegion = "us-central1";
const registryId = "";
const deviceId = "";

const version = 0;
const parentName = `projects/${projectId}/locations/${cloudRegion}`;
const registryName = `${parentName}/registries/${registryId}`;

admin.initializeApp();

var db = admin.database();

//Receiving Pub/Sub from device and adding data to Firebase
exports.receiveTelemetry = functions.pubsub
  .topic('telemetry-topic')
  .onPublish((data) => {
    const attributes = data.attributes;
    const message = data.json;
    const deviceId = data.attributes.deviceId;
    
    const location = {
      lat: message.lat,
      lng: message.lng
    };

    return Promise.all([
      updateCurrentDataFirebase(location, deviceId)
    ]);
  });

function updateCurrentDataFirebase(data, deviceId) {
  var d = new Date();
  var timeInMillis = d.getTime();
  var ref = db.ref(`/devices/${deviceId}/${timeInMillis}`);
  return ref.set({
    lat: data.lat,
    lng: data.lng
  });
}

/*
  Updating config state in IoT Core
*/

exports.updateDeviceState = functions.https.onCall((data) => {
  getClient()
      .then(client => {
        console.log("before send config to device");
        return sendConfigToDevice(client, data);
      })
      .then(response => {
        console.log('SendConfigToDevice:', response);
      })
      .catch(err => {
          console.log('Exception catching:', err);
        });
});

function sendConfigToDevice(client, data) {

  console.log("should send config");
  
  const myMap = new Map(
      Object
          .keys(data)
          .map(
              key => [key, data[key]]
          )
  )

  console.log("data: " + myMap.get("state"));

  const newData = "{ \"state\": " + myMap.get("state") + "}";
  const binaryData = new Buffer(newData, 'utf-8').toString('base64');

  const request = {
    name: `${registryName}/devices/${deviceId}`,
    versionToUpdate: version,
      binaryData: binaryData
  };


  return new Promise((resolve, reject) => {
      client.projects.locations.registries.devices.modifyCloudToDeviceConfig(request,
        (err, response) => {
          if (!err) {
            console.log("should modify cloud config");
            resolve(response);
            return;
          }
          console.log("reject on modifyCloudToDeviceConfig");
          reject(err);
        }
      );
    }); 

  }

function getClient() {
  return new Promise((resolve, reject) => {
    const serviceAccount = JSON.parse(fs.readFileSync('SmartBike.json'));
    const jwtAccess = new googleapis.auth.JWT();
    jwtAccess.fromJSON(serviceAccount);
    jwtAccess.scopes = API_SCOPES;
    googleapis.options({auth: jwtAccess});
    const discoveryUrl = `${DISCOVERY_API}?version=${API_VERSION}`;
    googleapis.discoverAPI(discoveryUrl, {}, (err, client) => {
      if (err) {
        console.log('Error during API discovery', err);
        return reject(err);
      }
      return resolve(client);
    });
  });
}
Web VisualizerHTML
Removed config data, file paths, api keys
<!DOCTYPE html>
<html>
  <head>
    <title>Route</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <style>
      /* Always set the map height explicitly to define the size of the div
       * element that contains the map. */
      #map {
        height: 100%;
      }
      /* Optional: Makes the sample page fill the window. */
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
    </style>

    
  <script src="https://www.gstatic.com/firebasejs/4.12.1/firebase.js"></script>
  <script>
    // Initialize Firebase
    var config = {
      apiKey: "",
      authDomain: "",
      databaseURL: "",
      projectId: "",
      storageBucket: "",
      messagingSenderId: ""
    };
    firebase.initializeApp(config);
  </script>

  </head>
  <body>
    <div id="map"></div>
    <script>
      var map;

      function initMap() {
        map = new google.maps.Map(document.getElementById('map'), {
          center: {lat: 40.01972, lng: -105.2764},
          zoom: 18
        });

        var database = firebase.database();
        var locations = firebase.database().ref('devices/Helium-6081f9fffe0020be');

        locations.on('value', function(snapshot) {
          var path1HeatmapData = [];
          snapshot.forEach(function(childSnapshot) {
            var childData = childSnapshot.val();
            path1HeatmapData.push(new google.maps.LatLng(childData.lat, childData.lng));
          });

          var path1Heatmap = new google.maps.visualization.HeatmapLayer({
            map: map,
            data: path1HeatmapData,
            radius: 20,
            opacity: 1.0
        });

        });

        fetch('FILE_PATH')
        .then(response => response.text())
        .then(text => {
          lines = text.split('\n');
          var pathData = []
          var color;

          for( var i = 1; i < lines.length; i++ ) {
            if( lines[i].includes("<name>") ) {
              var path = new google.maps.Polyline({
                path: pathData,
                geodesic: true,
                strokeColor: color,
                strokeOpacity: 1.0,
                strokeWeight: 4
              });

              pathData = [];
              path.setMap(map);
            } else if( lines[i].includes("color") ) {
                color = "#" + lines[i].substring(9, 15);
            } else if( !lines[i].includes("coordinates") ) {
                var splitData = lines[i].split(",", 2);
                pathData.push({ lat: parseFloat(splitData[0]), lng: parseFloat(splitData[1])});
            }
          }
        })
      }
    </script>
    <script src="https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=visualization&callback=initMap"
    async defer></script>
  </body>
</html>
coordinatesnewlines.pyPython
import re

def find_between( s, first, last ):
    try:
        start = s.index( first ) + len( first )
        end = s.index( last, start )
        return s[start:end]
    except ValueError:
        return ""

f = open("trimmed3.txt", "r")
lines = f.readlines()
f.close()
f = open("trimmed4.txt", "w")
regex = re.compile('^<coordinates>')

for line in lines:
	if re.match(regex, line.lstrip()):
		line = find_between(line, "<coordinates>", "</coordinates>")
		f.write("<coordinates>\n")
		line = line.lstrip().replace(' ', '\n')
		f.write(line)
		f.write("\n</coordinates>\n")
	else:
		f.write(line);

Custom parts and enclosures

Casing bottom
Casing top

Schematics

Schematics for Tracker
Add Helium Atom shield to the top of the board. Other pins are as shown using the shield.
Tracker ysbzq02uiu

Comments

Similar projects you might like

Arduino Bluetooth Basic Tutorial

by Mayoogh Girish

  • 456,398 views
  • 44 comments
  • 242 respects

Home Automation Using Raspberry Pi 2 And Windows 10 IoT

Project tutorial by Anurag S. Vasanwala

  • 285,646 views
  • 95 comments
  • 672 respects

Security Access Using RFID Reader

by Aritro Mukherjee

  • 230,066 views
  • 38 comments
  • 239 respects

OpenCat

Project in progress by Team Petoi

  • 197,246 views
  • 154 comments
  • 1,372 respects
Add projectSign up / Login