Cellular Signal Heatmap With Raspberry Pi Pico And Gps

Photo of RobLauer

Made by RobLauer / IoT

About the project

Create a Google Map heatmap with cellular and GPS data acquired by a Raspberry Pi Pico and Notecard, using Python and Google Cloud Platform.

Project info

Difficulty: Easy

Platforms: GoogleRaspberry PiBlues Wireless

Estimated time: 5 hours

License: MIT license (MIT)

Items used in this project

Hardware components

Breadboard (generic) Breadboard (generic) x 1
Solderless Breadboard Power Supply Module Solderless Breadboard Power Supply Module x 1
DFRobot I2C 16x2 Arduino LCD Display Module DFRobot I2C 16x2 Arduino LCD Display Module x 1
blues wireless Notecarrier-AL blues wireless Notecarrier-AL x 1
blues wireless Notecard blues wireless Notecard x 1
Raspberry Pi Pico Raspberry Pi Pico x 1

Software apps and online services

blues wireless Notehub.io blues wireless Notehub.io
Google Cloud Platform Google Cloud Platform
Google Maps Google Maps

Story

This project originally appeared on hackster.io.

A heatmap overlaid on satellite imagery is one of the more tantalizing means of displaying map-based data. Considering my newfound love for the Raspberry Pi Pico, and having seen how easy it can be to add cellular connectivity to the Pico, why not combine these loves in a new project?

Today we are going to build a cellular- and GPS-enabled IoT solution that collects cell signal strength and location data at defined intervals, stored in the cloud, with a web app to display said data in a heatmap.

A "heatmap" is a visual representation of data where values are translated into colors.

If you'd like a quick 1.5 minute overview of the project, check out this video:

Specifically we will be developing with:

If Google Cloud Platform is a turnoff, you can absolutely swap in your cloud provider of choice for most of the functionality covered. 🤗

So at the highest level we are:

Ready? Let's get started!

Position Data with GPS and Cellular 🗺️

If our goal is to collect cell signal strength AND location data at periodic intervals, ideally we want a hardware solution that can handle both simultaneously. All the while being aware that GPS can be very power-hungry.

We can handle this with the Notecard from Blues Wireless. The Notecard is a low power cellular and GPS device-to-cloud data pump that comes with 500 MB of data and 10 years of service for $49. No activation charges, no monthly fees.

To make things even easier, Blues Wireless also provides a series of expansion boards (called Notecarriers) to host the Notecard. For this project, we will use the Notecarrier-A, a board with pre-soldered female headers allowing access to all the pins on the Notecard edge connector.

Now here is the really important part:

The Notecard ships preconfigured to communicate with Notehub.io, the Blues Wireless service that enables secure device-to-cloud data flow. Notecards are assigned to a project in Notehub, which then routes data to your cloud of choice (in our case, that'll be Google Cloud Platform).

Let's Build! 🏗️

Here's a quick look at what we are going to build:

Our Raspberry Pi Pico can supply all the power our Notecarrier needs. Use jumper wires to connect 3V3(OUT) and GND on the Pico to a power rail on the breadboard. Next, connect the power rail to BATT and GND on the Notecarrier.

We'll be using the I2C bus for communication between devices, so we can connect SCL (the clock line) and SDA (the data line) pins on the Notecarrier to any open I2C headers on the Pico (I used pins 4 and 5 which place the Notecarrier on I2C1).

The one downside of the Raspberry Pi Pico is the lack of labels on the top of the board. Therefore, it's always worthwhile to keep a pinout handy.

Image source: Rasperry Pi Foundation

Next up is the LCD module. I really like using the 1602A LCD because it has a pre-soldered backpack making I2C and power connections a snap.

Connect GND and VCC on the LCD to the appropriate spots on your breadboard's power rail. The Pico can only supply 3.3V out, but the LCD works best (brightest) with 5V. So, while you can connect the LCD to the same power rail that the Pico is powering, the screen will be fairly dim.

A better solution is a battery-powered breadboard power supply. I like using this with a 9V battery as it can supply 5V of regulated power to whichever rail you want (in this case the opposite rail that the Pico is already powering).

So now we can supply 3.3V to the Notecarrier and 5V to the LCD! 🔌

Finally, the LCD is going to use the same I2C bus as the Notecarrier. Connect SDA and SCL from the LCD module to the same pins you used for the Notecarrier.

And that's it! Refer to the completed hardware assembly image above if need be.

Let's Write Some (Micro)Python! 🐍

With our physical hardware configured properly, it's time to program our Pico with some MicroPython.

TIP: Want to skip the step-by-step code writing exercise? You can always view this repository with the completed assets.

Set Up Your Pico for MicroPython

The Pico supports MicroPython, just not out of the box. So if you haven't already, you'll need to flash your Pico's firmware.

To install MicroPython, just follow the instructions provided under "Getting Started with MicroPython" in the Raspberry Pi docs.

Start Your IDE

Boot up your favorite code editor. I gravitate towards Visual Studio Code and the Pico-Go extension, but Thonny is a great little IDE as well.

Create two files: main.py and keys.py. You can probably guess that main will store our application logic and keys will store sensitive data that we don't want to commit to public source control.

Our keys.py file is only going to store one key, and that's the Notecard's ProductUID. This associates our Notecard with our cloud-based Notehub project. We'll come back to this in a bit, but for now, just add this line:

PRODUCTUID = "com.company.name:project"

Grab Some External Libraries

Our project is going to use three external libraries. Two for communicating with the LCD and one for communicating with the Notecard.

Download the latest note-python library. Create a lib directory on your Pico and copy the notecard directory in there.

Next, download lcd_api.py and machine_i2c_lcd.py from the completed project's source repo. These can be copied to the Pico next to main.py.

Our completed file and folder structure on the Pico should look like this:

/lib 
  /notecard
    card.py
    env.py
    ...
keys.py
lcd_api.py
machine_i2c_lcd.py
main.py

Imports and Constants

Open up main.py and add the following import statements to the top of the file:

from machine import I2C, Pin
from machine_i2c_lcd import I2cLcd
from time import sleep
import keys
import notecard

Create a variable that will hold the Notehub ProductUID from keys.py:

PRODUCTUID = keys.PRODUCTUID

Initialize Our Components

Next we need to initialize the I2C bus, our LCD module, and the Notecard.

Since I used GPIO pins 2 and 3 for I2C (which correspond to the I2C1 bus), I can initialize I2C with:

i2c = I2C(1, sda=Pin(2), scl=Pin(3))

We need to figure out the I2C address of the LCD module. To do so, there is a helpful scan method we can use. I'm referencing the 2nd device on the I2C bus with [1] as the Notecard has a lower I2C address (0x17 vs 0x27 if you're curious):

lcd_i2c_addr = i2c.scan()[1]

Finally, we initialize the LCD (2 rows with 16 characters on each row):

lcd = I2cLcd(i2c, lcd_i2c_addr, 2, 16)
lcd.clear()

Next we'll want to set up our Notecard. The note-python library comes with a handy OpenI2C method that does the dirty work for us:

card = notecard.OpenI2C(i2c, 0, 0, debug=True)
print("Successfully Connected to Notecard!")

Optionally, we can perform a quick reset of the Notecard. I find this convenient when prototyping a lot of different projects that share the same Notecard!

req = {"req": "card.restore", "delete": True}
rsp = card.Transaction(req)
sleep(2)

Next up, let's configure this Notecard to speak with Notehub (recall that Notehub is the Blues Wireless cloud service that is going to route data to our cloud application).

req = {"req": "hub.set"}
req["product"] = PRODUCTUID
req["mode"] = "periodic"
req["inbound"] = 720
req["outbound"] = 15
rsp = card.Transaction(req)

What's going on here? We are creating a hub.set request to set some Notecard/Notehub syncing parameters. We are setting the ProductUID and defining a periodic cellular connectivity mode. The inbound parameter means the Notecard will connect to Notehub once every 720 minutes to check for any data it needs to download. More importantly for us, the outbound parameter tells Notecard to send collected data to Notehub every 15 minutes (but only if there is data to send).

NOTE: Your GPS signal will be interrupted when the Notecard syncs to Notehub, so you can extend the outbound value to avoid interruptions.

We can now set the appropriate GPS mode on the Notecard. Since our program relies on a regular stream of GPS data, we'll want to use continuous mode:

req = {"req": "card.location.mode", "mode": "continuous"}
rsp = card.Transaction(req)
sleep(60) # optional delay to give gps time to acquire signal

Finally, we need a boolean variable that tells us whether or not GPS is active and receiving latitude and longitude coordinates:

is_gps_active = False

Supporting Functions

At the bottom of main.py we need to add a while loop and associated function that will check to see if GPS is active:

  1. def check_gps(rsp):
  2. """ checks if gps module is active with location """
  3. if ("{gps-sats}" in rsp["status"] and "lat" in rsp):
  4. return True
  5. else:
  6. return False
  7.  
  8.  
  9. while not is_gps_active:
  10. """ if gps module is active, start tracking location """
  11. req = {"req": "card.location"}
  12. rsp = card.Transaction(req)
  13.  
  14. if check_gps(rsp):
  15. is_gps_active = True
  16. lcd_msg("GPS ACTIVATED")
  17. track_location(rsp["lat"], rsp["lon"])
  18. else:
  19. lcd_msg("GPS INACTIVE")
  20. sleep(20)
NOTE: Make sure the while loop remains at the bottom of main.py as we add additional functions.

We know that GPS is active if:

  • "{gps-sats}" is returned from a card.location request, signifying one or more GPS satellites have been found.
  • The lat key is found in the JSON response.

If GPS is active, we display a message on the LCD module with the lcd_msg function (print is nice to use when debugging):

  1. def lcd_msg(msg):
  2. """ displays a message on the lcd screen """
  3. lcd.clear()
  4. sleep(0.5)
  5. lcd.putstr(msg)
  6. print("*****" + msg + "*****")
  7. sleep(0.5)

Continuing forward, when GPS is active we are ready to start tracking our location with the track_location function:

  1. def track_location(lat, lon):
  2. """ if gps is active and location has changed, create a note in notehub """
  3. loc_changed = False
  4.  
  5. while not loc_changed:
  6. req = {"req": "card.location"}
  7. rsp = card.Transaction(req)
  8.  
  9. # double check that gps is still active!
  10. if check_gps(rsp) and rsp["lat"] != lat and rsp["lon"] != lon:
  11. loc_changed = True
  12. lcd_msg("GPS LOC CHANGED")
  13. new_lat = rsp["lat"]
  14. new_lon = rsp["lon"]
  15. bars = get_cell_bars()
  16. add_note(new_lat, new_lon, bars)
  17. sleep(10)
  18. # start tracking location again!
  19. track_location(new_lat, new_lon)
  20. else:
  21. lcd_msg("NO GPS CHANGE")
  22. sleep(10)

There is a lot going on in these functions, so let's focus on the important parts:

  • We double check that GPS is still active (can never be too careful!).
  • We check to see if our lat/lng coordinates have changed.
  • We get the cell signal strength in the get_cell_bars function and queue data to Notehub with the add_note function (see below).
  • Finally, we start tracking our location again by calling track_location with the updated lat/lng coordinates.

Here are the two remaining functions referenced above:

  1. def add_note(lat, lon, bars):
  2. """ uploads the note/event to notehub.io """
  3. req = {"req": "note.add"}
  4. req["file"] = "bars.qo"
  5. req["body"] = {"lat": lat, "lon": lon, "bars": bars}
  6. rsp = card.Transaction(req)
  7. lcd_msg("NOTE SAVED")
  8.  
  9. def get_cell_bars():
  10. """ gets the cell signal strength from card.wireless """
  11. req = {"req": "card.wireless"}
  12. rsp = card.Transaction(req)
  13. return rsp["count"]
TIP: The Blues Wireless Developer Portal provides thorough documentation of all the Notecard APIs.

And that's it! The program on our Pico should be 100% complete and ready to start gathering data.

Before we put it into action, let's see how we finish syncing Notecard with Notehub and what we then can do with our gathered GPS data.

Pumping Data to the Cloud ☁️

Cloud Part I: Google Cloud Platform

Google Cloud Platform (GCP) is Google's answer to Amazon's AWS and Microsoft's Azure. Providing a comparable set of features, GCP is an intriguing third option of big cloud providers. Personally I find AWS and Azure to be more powerful, but GCP more user-friendly. Your mileage may vary.

We need three things from GCP:

  • A place to store our accumulated GPS data.
  • A means of accepting said data from Notehub.
  • An API key for using Google Maps.

We can check the first two requirements off with Cloud Firestore and Cloud Functions, respectively.

Let's kick off our Google Cloud Platform journey by creating an account at console.cloud.google.com.

Create a new project, naming it whatever you'd like:

NOTE: In Google's documentation you'll see references to both "Google Cloud" and "Firebase" that reference the same products and functions. It's confusing, but some features from Firebase are also available in GCP. 🤷

Cloud Firestore

Firestore is used for storing, querying, and syncing data. It's a fast and relatively easy-to-use NoSQL database.

Navigate to the Firestore menu item (not to be confused with Filestore!).

Assuming this is the first time you've created a Firestore instance, you'll be prompted to choose between "Native" and "Datastore" mode. You're welcome to read about the differences, but generally speaking you're best off with "Native":

Choose a geo location close to you and create your database.

Lastly, you'll be prompted to create your first collection (or "table" for you relational db folks). Name your collection something memorable (e.g. "mapdata").

NoSQL databases, you either love 'em or hate 'em! One big advantage of NoSQL databases is that we don't have to pre-define any additional fields in the data stored...so that's all the Firestore configuration we have to do!

Cloud Functions

Google Cloud Functions are serverless functions you write (in almost any language) that are hosted and executed in Google's cloud. This provides a great way for us to send JSON data from Notehub to GCP.

Navigate to the Cloud Functions menu item and create your first function.

Name it whatever you like and leave all of the defaults as-is, except to check "Allow unauthenticated invocations" under Authentication. In our case, access to this function will not be publicly known outside of Notehub, allowing us to avoid authentication struggles.

Copy that "Trigger URL" as you'll need it later.

On the next page, you may have to enable "Cloud Build API". You then can choose the language runtime. Since we started in MicroPython on the Pico, we might as well be consistent and choose Python. This will populate two files automatically, main.py and requirements.txt.

By default, Python cloud functions have access to numerous pre-installed packages. In our case, ironically the one missing is google-cloud-firestore. Open up requirements.txt and add:

google-cloud-firestore==2.0.2

Next, in main.py we are going to replace the default function with one super simple function called add_data:

  1. from google.cloud import firestore
  2.  
  3. def add_data(request):
  4. request_json = request.get_json()
  5. db = firestore.Client()
  6. db.collection(u'mapdata').document().set(request_json)
  7. return '', 200

What's happening here? The request parameter is literally going to be the JSON payload we send from Notehub. Here is an example payload Notehub will send:

  1. {
  2. "bars": 1,
  3. "lat": 43.09266,
  4. "lon": -89.49021,
  5. "location": "Shorewood Hills, WI"
  6. }

We are then adding a single document (a "record" for you relational folks) in the mapdata collection with document().set(request_json).

Lastly, we return a 200 status code to let Notehub know all went well with the transfer.

NOTE: Best practice dictates we should catch errors and gracefully fail instead of assuming every request ends in a 200!

Be sure to change the "Entry Point" to be the same name as the function, add_data, and then Deploy!

If you so desire, you can test the function by pasting in the example JSON payload provided above. If you navigate back to Firestore, you should see your test data appear like magic.

Google Maps

While we are still in the GCP console, let's take a moment to enable access to Google Maps.

Scroll all the way down to the Google Maps Platform menu option. Select and enable the Maps JavaScript API:

To enable access to Google Maps, you'll need to create a new API key. Navigate to the APIs & Services > Credentials menu option. Choose Create Credentials at the top of the screen and select API Key. You'll need this key for when you configure access to Google Maps in the web app later on.

Cloud Part II: Notehub

While we already went over Notehub and its role in our project, the high level workflow bears repeating:

  • GPS data is gathered and processed on the Notecard.
  • Notecard sends this data to Notehub over cellular.
  • Notehub routes this data to our cloud of choice.

Let's look at how we configure Notehub in a few simple steps.

  • Create your free account on Notehub.io.
  • Using the New Project card, give your project a name and ProductUID.Copy that ProductUID and paste it in keys.py on your Pico.
  • Copy that ProductUID and paste it in keys.py on your Pico.

Done? Great! When the program runs, Notecard will automatically associate itself with this Notehub project. Any notes (events) sent to Notecard will travel over cellular and show up in the Events panel when received:

Route Data to Firestore

Remember the "Trigger URL" for your Cloud Function? Great, because we need to now create a route for our data to travel from Notehub to GCP.

Navigate to the Routes menu option and click on Add Route.

Provide a name for your route and choose "Google Cloud Function" as the Route Type. Paste in the Trigger URL as the Route URL.

Under Notefiles choose "Select Notefiles". In the field provided, enter bars.qo (or whatever the note file name you specified in main.py on your Pico). We do this because we only want notes sent to GCP that are relevant to the data we want to display.

Next, we are going to use JSONata to create an expression that will allow us to format and process our JSON on the fly.

Why? The full JSON sent as part of the note contains meta data that we don't necessarily want to send along to GCP:

  1. {
  2. "event": "dbfb2474-4a4a-4d3d-a171-b122be5f1f",
  3. "session": "ed8aee23-1f30-4e33-b252-0fd5a5d761",
  4. "device": "dev:798357841085845",
  5. "product": "product:com.xxx.yyy:proj",
  6. "received": 1616534703.063639,
  7. "routed": 1616534707,
  8. "req": "note.add",
  9. "when": 1616534635,
  10. "file": "bars.qo",
  11. "body": {
  12. "bars": 1,
  13. "lat": 43.092329999999993,
  14. "lon": -89.48977
  15. },
  16. "where_olc": "86MG3GR6+X2Q4",
  17. "where_lat": 43.0924525,
  18. "where_lon": -89.48988671875,
  19. "where_location": "Middleton, WI",
  20. etc...
  21. }

This is where JSONata comes in handy. The JSONata Expression will be this:

{"bars":body.bars, "lat":body.lat, "lon":body.lon, "location": where_location}

Make sure your route is enabled and save the changes.

We're almost done! So far we have:

  • Collected GPS data on the device.
  • Sent the data to Notehub.
  • Used Notehub to process the data and deliver it to a Cloud Function on GCP.

Last up is creating the web app to display our heatmap with Google Maps.

Displaying our Heatmap on the Web 🔥

Let's wrap up our project by architecting a relatively simple React web app that will use Google Maps to generate a heatmap of our cell signal data.

Again, you can find all of this code in the GitHub repository if you want to skip the narrative!

Initializing the React App

The defacto means of starting a new React app is by using Create React App. This CLI is a great way to bootstrap a new app. To install, use this command in your terminal:

npx create-react-app map-app
cd map-app

Next, add the react-google-maps/api and the firebase libraries to access Google Maps and Cloud Firestore respectively:

yarn add @react-google-maps/api
yarn add firebase

React Components

Generally speaking, React apps are made up of multiple function and class components.

Our React app has three components:

  • App.js is our root-level component.
  • MapContainer.js loads the Google Maps script and provides a host container for...
  • Map.js which shows our map.

In the interest of space (and time) we aren't going to cover every aspect of the web app (especially since the full source is available here). However, we should cover a few critical aspects.

Firebase?

Right. yarn add firebase is an example of the Firebase vs Google Cloud Platform confusion. As noted above, both Firebase and GCP offer access to Firestore. The APIs for accessing Firestore are in the Firebase SDK.

We are going to use the Firebase SDK to access our data from Firestore. For example, to pull in all of the data from our mapdata collection, we use this code in the Map.js component:

let mapData = db.collection('mapdata');
let allPoints = await mapData.get();

Map Container

Our MapContainer.js component is the host of our map. Why do we have to use separate components? Welcome to asynchronous JavaScript land!

MapContainer.js loads the Google Maps script. But you can't use it until it's loaded, right? Thankfully the react-google-maps/api library exposes LoadScript which plays a key part in lazy-loading Map.js:

  1. <LoadScript
  2. googleMapsApiKey={getGoogleMapsKey}
  3. libraries={['visualization']}
  4. >
  5. <Suspense fallback={<div>Loading...</div>}>
  6. <Map></Map>
  7. </Suspense>
  8. </LoadScript>

The Map

Finally, our Map.js component exposes the core Google Map, allowing us to set the center of the map (shortcut alert: we are just using the first record from the mapdata collection for the center) and the HeatmapLayer which exposes the Google Maps heatmap API.

  1. <GoogleMap
  2. mapContainerStyle={containerStyle}
  3. center={this.state.center}
  4. zoom={13}
  5. mapTypeId={'satellite'}
  6. >
  7. <HeatmapLayer data={this.state.mapPoints} options={options} />
  8. </GoogleMap>

The data property of the HeatmapLayer is an array of all the returned data points from Firestore.

A single object in the array is made up of a location (lat/lng coordinates) and a weight integer (cellular signal strength). The higher the weight, the more intense the heatmap color.

  1. let point = {
  2. location: new google.maps.LatLng(doc.get('lat'), doc.get('lon')),
  3. weight: doc.get('bars'),
  4. };

We can also tweak the display of the heatmap by supplying options to the HeatmapLayer element. We specify the radius, or how far we want our heatmap colors to spread around a point.

const options = {radius: 40};

Running the Web App

With our web app built, back in the terminal we can:

  • Install dependencies with yarn install.
  • Run the app with yarn start.

Since at this point we don't have any real data accumulated, we can test out our map by using the testPoints array in Map.js instead of the data pulled from GCP.

Go Gather Some Data 🚗

We are ready to take our project on the road! I attached a portable battery pack to my Pico and placed the solution on my car's dashboard (the GPS antenna needs an unobstructed view!).

Now Run the Web App

Back home with our accumulated data, it's time to try out the map with some real world results.

GPS Gotchas

If you run into any issues gathering GPS data, be patient, as GPS can be very fickle. It's important to remember that the Notecard is a very power-conscious piece of hardware, and GPS is very power-hungry, so don't expect the same results as using a dedicated GPS receiver.

Take note of these "gotchas" when working with GPS:

  • Regardless of the mode you are using, the GPS module will only be activated when it's moving.
  • The GPS module will only activate itself after the cellular modem has acquired a signal. It can take 2-3 minutes before a reliable signal is acquired.
  • The Notecarrier-A (used in this project) has a relatively weak GPS antenna compared to the other Notecarriers (which can also use active external GPS antennas).

Next Steps

Congrats on making it this far as there is A LOT to comprehend when building out a full solution containing components from device to cloud to web.

Even if you just skimmed through, I hope it was evident how easily we can now take on-device data and utilize a modern cellular/GPS device to efficiently move data to our cloud application of choice.

Want some next steps?

Happy hacking...and mapping! 🔥🗺️

Code

Github

https://github.com/rdlauer/notecard-heatmap

Github

https://github.com/blues/note-python

Credits

Photo of RobLauer

RobLauer

Director of Developer Relations @ Blues

   

Leave your feedback...