Paul Ruiz
Published

Bike Route Data Gatherer

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

IntermediateFull instructions provided4 hours13,600

Things used in this project

Story

Read more

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.

Code

Firebase Function

JavaScript
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.py

Python
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.py

Python
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.py

Python
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.py

Python
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 Tracker

C/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.java

Java
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 Updater

XML
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 Code

JavaScript
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 Visualizer

HTML
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.py

Python
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);

Credits

Paul Ruiz

Paul Ruiz

19 projects • 83 followers
Developer Relations Engineer @ Google ML/AI. IoT and mobile developer. Formally robotics.

Comments