Click here to Skip to main content
15,217,814 members

GarageWatcher - A Full-Stack IoT Project, from Hardware to App

Rate this:
5.00 (12 votes)
Please Sign up or sign in to vote.
5.00 (12 votes)
2 Jun 2020CPOL
Musings, tips, and lessons learned from developing a physical device, serverless back-end, and Android app
A beginner's endeavor to develop an integrated solution using an ESP-32 microprocessor, Google Firebase's serverless platform, and an Android phone app. In this article, you follow me as I frame the problem, weigh my options, write my code, and finally deploy the solution. With this information, you should be able to pursue a similar project of your own.

Preface

Imagine that you’re driving down your neighborhood streets, humming along to your radio and enjoying the bright sunny day, when the inevitable question flashes across your mind.

“Did I leave the garage open?”

Panic! Terror! Dread! A torrent of emotions washes over you as you realize that maybe, just maybe, you forgot to do something that was once habit. But you can’t be certain that you didn’t close it, either. Is it worth the trip to go home and double check? Can you trust your instincts? Is the quick trip to the store a safe period of time to leave it open?

The easy solution to this, of course, is to build a moat on your driveway that refuses to open until you’ve closed the garage door. That, or you purchase one of many IoT-enabled devices that hardwire themselves into your garage door opener, spend a day trying to get it to connect to the proper network, then worry for the rest of your life that someone else is going to steal your login credentials and play red-light-green-light with your house.

Instead, let’s make our lives a little more difficult – and fulfilling! – by building our own IoT device, serverless cloud backend, and an app.

Introduction

In this paper, I will take you through the process and lessons learned from this complex set of projects:

  1. Building a battery-operated ESP-32 device that suits our premise
  2. Assembling a backend on the cloud via Firebase
  3. Writing an Android app to retrieve and affect our data

I will take this opportunity to say that I am by no means an expert – or even a novice, really – at any of these. In fact, you will see that I happen to stumble my way through many of the steps I take, driven only by my curiosity and the necessarily sadistic nature of being multidisciplinary. Please don’t take my word (or work) as gospel. In fact, I implore you to do better. There’s a reason I’m not going to share the whole source code for these projects – they are horrible amalgamations of several days’ work, and I’m not proud of them. The snippets I provide here, however, should be good enough reference that you can follow along and assemble something beautiful on your own.

With that out of the way, let’s enjoy this as a learning experience, shall we?

The Device

Image 1

Building the Hardware

A few months ago, I happened to discover the NodeMCU ESP-32S Devkit in a college course where we built an app-controlled toy car. Previously, I had mainly stuck with Arduinos and their Adafruit variants, with which I lamented that getting internet connectivity was an expensive and complicated process. The only alternative I knew of would be a Raspberry Pi, which was often overkill for the small projects I had in mind, and also carried too much overhead. Reading through the specifications for the ESP-32 suddenly unlocked a whole new set of rules I could play by, and I grabbed a few WROOM modules as quickly as I could.

For this project, I played with several operational variations.

  • Have the ESP-32 be on at all times, sending a continuous stream of data to the cloud. This could be done by directly powering the device from a wall socket. Unfortunately, sockets are not common in my garage, and taping long speaker wires across the ceiling seemed inelegant.
    • The sensor could be either a contact switch at the bottom of the garage door, or a laser interrupt. You could also mount the sensors at the top of the door when it is closed, to avoid rainwater from accidentally seeping in.
    • Solar power would have been a nice addition, except that sunlight isn’t quite as abundant a resource where I live.
  • Have the ESP-32 be powered by the garage door switch. This would be risky, since you’d need to fiddle with the existing wiring, and I wasn’t confident I could find the correct documentation on the function of each wire. Also, the problem scope here was never about remotely controlling the door – just a notification or status indication of the door being open.
  • Have the ESP-32 be powered only when the door is opening or opened. This would reduce power consumption, but it would be hard to tell when the door was closed.

Ultimately, I selected the last option. Since the most important information was if the door had been left open, I used that as the deciding factor.

Now, how would I go about building a device that would only power on when the door opened? As it turns out, garage doors don’t raise straight up (at least, mine don’t) – they fold horizontally across the ceiling. This meant that a physical tilt-switch would serve as the perfect switch to turn our device on and off. By installing the switch a few degrees off perpendicular with a panel of the closed door, as soon as the panel begins retracting into the ceiling, the circuit is closed.

If you’re unfamiliar with tilt switches, they’re little glass capsules with a bead of liquid mercury inside and two wires. The bead is always in contact with the longer wire, but if you tilt the capsule in the right direction, the mercury falls in contact with the shorter wire, which completes the circuit. They’re susceptible to inertia, so shaking it could theoretically set it off as well, but fixed against a garage door, I wasn’t worried about it.

Image 2

I arranged the circuit with a 220µF capacitor to dampen any voltage shocks from the mercury bouncing in the switch – I’m not an electrical engineer, but simply throwing 6 volts repeatedly against the ESP-32 seemed like a bad idea and if I remember my ECE course correctly, capacitors help dampen that sort of noise. In hindsight, perhaps an inductor would have been more useful, but alas, I didn’t have any readily available.

The 6 volts are sourced from four AA batteries, which I naively predict will last at least several months, given that the device is off most of the time.

A buzzer is connected to pin 12 of the ESP-32, to allow for some audible response, and the ESP-32’s built-in status LED is connected to pin 2. The buzzer is a little quiet, so throwing a transistor on there to feed it 6V instead of a measly 3V would probably help, but I didn’t have any on hand.

Image 3

I then encased everything in a plastic food container, to keep out unwanted moisture, and attached the whole box against the inside of my garage door's second-highest panel. I tried duct tape at first, but when the weight of the four AA batteries seemed too much, I drilled a couple of holes through instead. So far, it's stayed on pretty well. Word of advice: leave plenty of space to plug in a USB to your microcontroller, in case you need to update it. Everything in my case is hot-glued down, so removing it is a much bigger hassle. Luckily, I left enough clearance to comfortably slide in my MicroUSB cable without risking bent components.

With the device physically assembled, it’s time to look at the code.

Building the Code

The premise of the device is that it powers on only when the door is open, so the device code itself acts as the ‘opening’ sensor. As such, all we need to do is connect to WiFi, send a signal to the cloud, and keep sending signals until we’re told to sleep.

For this project, I stuck with using the Arduino IDE to code and compile for the ESP-32. I could have installed MicroPython on it and ran on that instead, but it’s a non-trivial process to do so, and I didn’t really need any Python packages to get what I needed from the ESP-32.

The first step would be to connect to the internet. I simply used a commonly-found snippet to do so:

// connect to wifi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Establishing connection to WiFi..");
}

In addition to serial output, I also set up a status light code that would let me know when certain milestones were hit in the code. On boot, the light would flash once with on() to indicate it had began searching for WiFi:

  Serial.begin(9600);
  pinMode(LED, OUTPUT);
  pinMode(BUZZ, OUTPUT);

  // start indication that power is available
  on(50); // value indicates ms to remain on

  // connect to wifi

Followed by five quick flashes to indicate connection success:

// indicate wifi found
Serial.println("WiFi found!");
for (short i = 0; i < 5; i++){
  on(75);
  delay(50);
}

After establishing a connection, the device would send a notification to the cloud letting everyone know that the door was now open.

The question at this point was ‘what sort of infrastructure would make the most sense?’. I wanted to avoid running a VM or webserver, both because I knew the downtime far exceeded the uptime and also because I wanted to foray into the wilderness that is serverless applications. As you’ll see later in this paper, I ultimately ended up using Firebase’s Functions for this.

The ‘awake’ signal sent by the ESP-32 would trigger an HTTP function, which would then update some other stuff while returning some key information for the ESP-32 to abide by.

void initialChirp() {
  // send a hello message
  String result = sendAwake();
  Serial.println(result);
  // result should tell us delay values in seconds 
  // {"result":"good morning","d1":"#","d2":"#","quiet":"?"}

The sendAwake() function creates an HTTP client, connects it to my cloud function, and performs some parsing on the returned values:

String sendAwake() { // adapted heavily from randomnerdtutorials
  HTTPClient http;
  Serial.println("Sending awake...");
  http.begin("https://redacted-app-name.cloudfunctions.net/awake");

  // Send HTTP POST request
  int httpResponseCode = http.GET();
  String payload = "{}";
  if (httpResponseCode>0) {
    Serial.print("HTTP Response code: ");
    Serial.println(httpResponseCode);
    payload = http.getString();
    blinkCode(httpResponseCode);
  }
  else 
  {
    Serial.print("Error code: ");
    Serial.println(httpResponseCode);
  }

  // Free resources
  http.end();
  return payload;
}

The blinkCode() function simply takes the first digit of the HTTP return code and blinks the status LED that number of times, so a successful HTTP call (200) would blink twice:

void blinkCode(short value){
  for (int i = 0; i < value / 100; i++){
     on(50);
     delay(100);
  }
}

The return value of the sendAwake() function is actually the JSON-formatted contents of a database:

{"result":"good morning","d1":"#","d2":"#","quiet":"?"}

With this, we can initialize some settings for the immediate period, namely the two delays and a quiet-mode. The first delay is the amount of time between opening the door and sending a notification; this should be long enough that a normal open-close event can occur before it sends a notification. If your hands are full of groceries or if you’re backing down your driveway, a buzz on your phone hurrying you is not exactly welcome. The second delay would be the amount of time between notifications, which is meant to urge you to close the door. Quiet-mode enables or disables the buzzer, so if that sound is getting on your nerves, it can be remotely disabled without ripping the wires out.

Here’s how I extract each detail:

d1 = (result.substring(31, 33)).toInt();

d2 = (result.substring(41, 43)).toInt();

quiet = (result.substring(54, 55) != "f"); // defaults to true rather than false

I hear those of you grumbling about the use of Strings and substring here – bear with me. On a plain Arduino Uno at 16 MHz and 2KB of SRAM, these are expensive operations, but the ESP-32 gives me 240 MHz and 520 KB to play with.

Also, yes, if I try to set the delays to anything longer than about an hour and a half, the substrings don’t work – but I think the standard use case here is probably in the range of 5 to 20 minutes, so consider this an operational limit. If you’re going to be pursuing a similar project but you want to be able to go with however many digits you’d like, I suggest looking into strtok() or some other form of indexing and splitting. This might also be a good point to consider getting into MicroPython.

After we’ve set all our runtime settings, it’s time to get to work.

// first wait period
delay(1000 * 60 * d1);

// send a chirp
result = sendChirp();

sendChirp() is similar to sendAwake() except that the function that is called differs, and it only returns a simple message of “good chirp”. Would it have been better practice to have a single method with a parameter that dictated which cloud function to call? Yes. The only reason that I did not have that is because when I first began writing this code, I was trying to call cloud functions with a lot more data attached to the HTTP call, which meant I needed a dedicated method to organize all of that instead.

Now, after the ‘awake’ signal is sent and the first ‘chirp’ is sent, the code enters the main program loop.

void loop() {
  delay(1000 * ((60 * d2) - 15)); // minus 15 seconds to check for snooze

  // check and see if we've been told to snooze
  if (readSnooze()) {
    // go to sleep
    Serial.println("Going to sleep! Good night.");

    // funcs are auto-included via compiler
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
    esp_deep_sleep_start();
  } 
  else
  {
    // wait 15 seconds and then chirp
    delay(1000 * 15);
    Serial.println(sendChirp());
  }
}

The main loop’s job is to check if the ESP-32 has been ordered to ‘snooze’, and if not, to send yet another ‘chirp’. It achieves this by lightly sleeping for the amount of time described by d2, but 15 seconds before the next ‘chirp’, it asks the cloud function whether we have a ‘snooze’ order.

This setup begs the question: if I have a database available, why not just request the information directly from the database, especially since there’s no special parsing or processing required?

The answer is simple: authentication. A secure database is one that requires authentication, but that also means I have to do a lot more work to pull data from it. Especially working in an environment as unfamiliar to me as ESP-32, HTTP and REST protocols, Firebase, and all the other options I explored for my cloud back-end, simple was best. Calling cloud functions didn’t necessitate authentication, and as long as I ensured that the functions only covered a specific range of actions (and the functions aren’t leaked out), it’d be safe enough. I’ll get more into these in the Cloud section.

Back to the code; readSnooze() is another call to a cloud function that simply returns something like {"result":"true"}. All I have to do then is grab the first character of the result, i.e., ‘t’ or ‘f’, to know whether the device is meant to sleep. In fact, I do that in the function itself:

bool readSnooze() {
  HTTPClient http;
  ...
  String payload = "{}";
  ...
  http.end();
  return (payload.substring(11, 12) == "t");
}

If we are meant to ‘snooze’, there’s a large chunk of odd esp_ functions in the middle of that loop for that. The ESP-32 provides a bunch of options as to how low-power you want it to go, going as far as to disable most of the computing functions and only leaving the RTC running. I wanted to make use of this to conserve power, since we’re running on AA batteries, so even if the garage door has to remain open for an extended period, the CPU wouldn’t be continuously pulling a high current.

If you’re interested in making the ESP-32 go to sleep during the delay downtime as well, you can do so with esp_sleep_enable_timer_wakeup(time), which sleeps the CPU for the given number of microseconds – but this boot up does not continue from the last position in the stack; rather, it starts the entire program again, beginning from the setup() function. You’ll need to save state information about your current runtime, i.e., letting the device know this is a timer-wakeup and not a complete reset, by applying RTC_DATA_ATTR to some state-keeping variable. RandomNerdTutorials has a great article on all of this, which I recommend you look over.

With the ‘snooze’ check and CPU sleep call, that wraps up the code on the hardware. We can now go on to build the cloud functions and database that will support this code.

The Cloud

Selecting a Service, a.k.a. the Long and Winding Road

Over the course of developing this project from zero to completion, I experimented with a few cloud service providers. I began my search with my Android phone – id est, I needed to push data to it. Rather than set up an app that polled some server for status updates, I wanted to play with push notifications. As far as I could find, the most straightforward solution was to use Firebase Cloud Messaging (FCM). It makes sense – of course Google would tout their own service for their hardware.

In that regard, my hands were tied. I had never built an Android app before, so building a polling system that ran in the background from scratch was a bit of a challenge. Firebase would provide a well-documented set of SDKs which would let me focus more on learning everything else.

From there, I went first to the cloud platform I was most familiar with – Microsoft Azure.

Yes, it does seem roundabout that I would go for Microsoft immediately after establishing that Google had the tools I needed. However, keep in mind that at this point in time, my understanding of serverless architecture was vague at best – I was simply trying to stay in shallow waters.

Azure does provide Notification Hubs, which were compatible with FCM, so I thought this would be a good place to start. All that was left would be to create some Azure Functions to call, and I’d be good to go. However, this is also where the trouble began. The tutorials provided by Azure to set up the final Android app was rather muddy, and were based in Kotlin – which I was not quite ready to learn yet. Also, going from Notification Hub to FCM was like going from one messaging service to another, which I found silly.

Instead, I attempted to write an Azure Function that would use either REST or a Firebase SDK to directly request FCM messages. REST quickly went out the window, as my unfamiliarity with web protocols hampered my ability to comprehend what was happening. My only option, then, was to use the SDK in Python. That should have been easy enough; I’ve used Python pretty extensively since a couple of years ago, and I was looking forward to putting my knowledge to the test.

Unfortunately, something must have gone wrong because I ended up being completely helpless when trying to install the correct SDK on the functions. Azure’s KUDU Console failed on me, and refused to load – meaning I couldn’t manage the web app.

In hindsight, this was probably a blessing disguise, because the solution I ended up choosing was far more elegant than the patchwork that would have been required to get everything to communicate here.

The immediate second platform I went to look at was Amazon Web Services (AWS). At this point, I was still looking for the ability to write cloud functions that would use the Firebase SDK to communicate with FCM. Amazon’s Lambda service looked promising – until I tried to figure out how to install the SDK into the function.

Again – please keep in mind that I am a complete newcomer to all of this serverless architecture stuff. Lambda requires you to use Lambda Layers, their own system of uploading dependencies and such via CLI. Now, while this isn’t exactly rocket science, I am normally a PC developer. I understand packaging applications with libraries, I understand nesting Python directories to allow them to refer to one another. Being asked to upload a ZIP struck a nerve with my exhausted self, having already been stressed out about Azure and Firebase.

Fine, I thought to myself. I’ll just dive into Firebase and see what I can do there.

And with that, I braced myself to learn all about Firebase Functions, Realtime Databases, and FCM.

Organizing Everything

All the time spent researching and troubleshooting yielded a lot of time for me to think about how I wanted to arrange the data and messages sent from device to cloud to app, and back. The order of events ended up being the following:

  1. The device sends an ‘awake’ or ‘chirp’ message to a cloud function.
  2. The cloud function manipulates the database to indicate the device’s last known state.
  3. The cloud function, if necessary, asks FCM to send a push notification to subscribed devices.

I began writing the cloud functions alongside the development of the hardware code, so that I could make sure both sides conveyed information that the other could ingest. Firebase Functions, unlike Azure or AWS, didn’t support Python – so I ended up having to pick up a little bit of Javascript instead. And to be honest? It wasn’t as bad as I thought.

I ended up writing four functions:

exports.awake = functions.https.onRequest(async (req, res) => {...});

exports.chirp = functions.https.onRequest(async (req, res) => {...});

exports.getSnooze = functions.https.onRequest(async (req, res) => {...});

exports.setSnooze = functions.https.onRequest(async (req, res) => {...});

Their purposes are pretty straightforward. awake sets the ‘awake’ and ‘snooze’ flags in the database to ‘true’ and ‘false’ respectively, indicating that the device is active. getSnooze retrieves the ‘snooze’ value, as described earlier in the hardware code, whereas setSnooze forcibly sets ‘awake’ to ‘false', and ‘snooze’ to ‘true’. We’ll see how these two flags are used to determine the device state later in the App section.

Now, since I’m hosting the functions on Firebase itself, I’m given immediate access to the rest of my Firebase project. After a brief interlude of installing the Firebase CLI and getting a project set up, I could quickly connect to the project’s Realtime Database and manipulate entries as required.

The initial function file, index.js, provides a sample function to work with. From this, I developed my functions:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.awake = functions.https.onRequest(async (req, res) => {
// called when the esp32 wakes up. reset cache in database.
    var db = admin.database().ref()
    db.child('awake').set('true')
    db.child('snooze').set('false')

    var d1 = 20
    var d2 = 10
    var quiet = "true"

    db.child('d1').once('value').then(function(dataSnapshot){
        d1 = dataSnapshot.val();
        db.child('d2').once('value').then(function(dataSnapshot){
            d2 = dataSnapshot.val();
            db.child('quiet').once('value').then(function(dataSnapshot){
                quiet = dataSnapshot.val();
                res.json({result:'good morning', d1:`${d1}`, d2:`${d2}`, quiet:`${quiet}`})
            });
        });
    });
});
...

In order to access the database, there are two elements that are required: const admin = require('firebase-admin'), and var db = admin.database().ref(). With this, db now holds a reference to the database that can be read and written to in real time. However, while writing to the database uses a simple .set() method, retrieving information was a little more involved.

The Firebase SDK provides the db.child().once() asynchronous method for retrieval of the value stored in an element. Since I was just beginning to learn JS, I decided to use the quick-and-dirty method of nesting synchronous calls to once() and to return all the values once the final element had been polled.

It’s interesting to note that Firebase Functions will execute fine if you don’t end the function with a res.json({}) call, but the function execution returns an error code of -11 and takes a few extra seconds to transmit. As such, I would advise that you always return something from function calls, even if your calling code, i.e., sendChirp(), doesn’t really need to receive a payload:

exports.chirp = functions.https.onRequest(async (req, res) => {
    var topic = 'all';
    var message = {
        data: {
            note: 'chirp'
            },
        topic: topic
    };

    // Send a message to devices subscribed to the provided topic.
    admin.messaging().send(message)
    .then((response) => {
        // Response is a message ID string.
        console.log('Successfully sent message:', response);
    })
    .catch((error) => {
        console.log('Error sending message:', error);
    });
    res.json({result:'good chirp'})
});

The only thing left to do here on Firebase’s end is to set authentication for the database. Because I was only going to distribute my app to my family, creating user accounts for this was unnecessary, so I simply enabled anonymous login – i.e., any device with the app could manipulate the database, but someone just looking at the HTTP endpoint could not. This was done by simply checking that the authentication token was not non-existent:

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null "
  }
}

This would allow Firebase Functions within the same overall project and users with the app to have full access to the database.

With that, we’re ready to take on the beast that is the Android app.

The App

Prior to beginning this project, my experience with writing apps was next to zero. I’d deployed a few test WPF apps to my Windows Phone before that became yesterday’s news, and a few Unity Augmented Reality art projects to my Android. This time, I would be starting from scratch.

Android Studio started off fairly overwhelming. Let’s be honest – there’s a lot of buttons and settings, and as someone used to Visual Studio, this was a lot more dense than I’d expected.

Setting Up the Application

The first odd part of Android development is the build.gradle files and dependency sync. This was a realm I had never touched before – VS’s package manager and dependency importer (as well as Python’s pip install) have spoiled me, and having to add my own dependencies via package name strings in multiple places was rather confusing. I did eventually get the hang of the basics, but if you told me to write the app again without about twenty tutorials open at the same time, it would be a real struggle.

After adding the Google Services plugin in the project’s build.gradle,

buildscript {
    repositories {
        ...
    }
    dependencies {
        ...       
        classpath 'com.google.gms:google-services:4.3.3'

and then implementing all the necessary packages in the app’s build.gradle,

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'
...
dependencies {
    ...
    implementation 'com.google.firebase:firebase-messaging:20.2.0'
    implementation 'com.google.firebase:firebase-functions:19.0.2'
    implementation 'com.google.firebase:firebase-database:19.3.0'
    implementation 'com.google.firebase:firebase-auth:19.3.1'
}

the serious work could begin. As for the version numbers for the Firebase SDKs, I used the ones I found in online tutorials – I have no clue what the latest version numbers are. I’m sure there’s a resource out there that can tell you which to use, but hey – if it ain’t broke, don’t fix it. (I’m kidding, please fix it.)

The most important part of the app – the whole reason Firebase was integrated so deeply – is that it must receive push notifications. I began with this as the starting point, both in terms of the code as well as my learning experience with Android’s app infrastructure.

The way I understand it, given some shallow reading and surfing for tutorials, the AndroidManifext.xml file describes to the operating system what hooks should be applied to the app. In this case, the most important hook is to allow our app to listen for messages from Firebase:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="com.--.--">
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <application ... >
    ...
    <!-- [START firebase_service] -->
        <service

            android:name="com.--.--.MyFirebaseMessagingService"

            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>
</manifest>

Again, the way I understand it, this allows the project class “MyFirebaseMessagingService” to receive notifications by way of something else raising the ‘MESSAGING_EVENT’ action. This class, then, is required to look like this:

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onDestroy() {}
    @Override
    public void onCreate() {}
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {}
}

FirebaseMessagingService is a class from the Firebase SDK, which are imported as such:

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

The end result is that when an FCM message is received by your phone, onMessageReceived is called. The questions that follow this assertion are twofold: how does your phone connect to the messaging service in the first place, and also how do you get this service to listen when the app isn’t launched?

In order to connect the app to FCM services, Firebase provides a google-services.json file that contains the authentication information for your app. It seems that this file is automatically used when your app is built or executed to grant access to your Firebase project, and you’ll notice that com.google.gms.google-services is applied in the app’s build.gradle as well.

The other thing to take note of is the RECEIVE_BOOT_COMPLETED permission that is declared in the manifest. This is how we let Android know to start running the app whenever the phone boots up, so that messages can be immediately received without requiring the user to start the app manually. In order to hook this event up to the app, we have to direct it to another class via the manifest, like this:

<manifest ...>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    ...
    <application

        ...

        <receiver android:name=".autostart">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

This executes the ‘autostart.java’ class whenever the BOOT_COMPLETED event is raised.

Coding the App

Here, we get into finally building the pipeline within the app that allows the user to interact with the device status and so forth.

Similarly to the messaging service class, we must extend an Android base class in order to receive the BOOT_COMPLETED event and perform our initialization:

public class autostart extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent arg1) {
        Intent intent = new Intent(context, MyFirebaseMessagingService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(intent);
        } else {
            context.startService(intent);
        }
    }
}

Note that the Intent we’re creating launches an instance of MyFirebaseMessagingService, and not the app’s MainActivity. The reason for this is because we don’t need the app screen to pop up on launch; all we need to do is to ensure that the push notification listener is running, so that messages can get to the user.

From there, we need to also make sure that the listener is correctly listening to the correct ‘topic’, in a publisher/subscriber model provided by Firebase. We’ll do this any time the class is instantiated:

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    ...
    @Override
    public void onCreate()
    {
        FirebaseMessaging.getInstance().subscribeToTopic("all")
                .addOnCompleteListener(new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        String msg = "subscribed to 'all'.";
                        if (!task.isSuccessful()) {
                            msg = "couldn't subscribe to firebase!"; 
                        }
                        Log.d(TAG, msg);
                    }
                });
    }

This is a lot to say, subscribe our instance of FirebaseMessaging to the “all” topic and then report the result to the log. If you’ll recall, when we transmit a ‘chirp’ from the device to the Firebase Function, we provide both the data payload as well as a topic, via:

exports.chirp = functions.https.onRequest(async (req, res) => {
    var topic = 'all';
    var message = {
        data: {
            note: 'chirp'
            },
        topic: topic
    };
    ...
});

If you wanted to expand the topics, you could theoretically send notifications to users who opt-in for that type of notification; in my current model, there’s no immediate notification for when the garage door is opened, nor when another user snoozes or closes the garage – if this is information that could be useful to someone, they can choose to listen in to more detailed notifications without having to set up more listeners and FCM servers.

From here, what you want to do with the received push notification is up to you. In my case, I simply displayed a pop-up notification that alerted the user that the garage door had been open for an excessive time, with an option to immediately snooze further notifications for an hour:

Image 4

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
    ...
    NotificationManager notificationManager =
            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    popNotification(this, notificationManager);
}

public static void popNotification(Context dis, NotificationManager notificationManager){

    Intent intent = new Intent(dis, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    PendingIntent pendingIntent = PendingIntent.getActivity(dis, 0 , intent,
            PendingIntent.FLAG_ONE_SHOT);

    Intent IgnoreIntent = new Intent(dis, sendIgnore.class); 
    PendingIntent PendingIgnoreIntent = PendingIntent.getActivity(dis, 0, IgnoreIntent, 
            PendingIntent.FLAG_ONE_SHOT);
    String channelId = "com.--.--.app.channel";

    Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
    NotificationCompat.Builder notificationBuilder =
            new NotificationCompat.Builder(dis, channelId)
                    .setContentTitle("GarageWatcher Notification")
                    .setSmallIcon(R.drawable.ic_stat_ic_notification)
                    .setContentText("Garage door is open!")
                    .setAutoCancel(true)
                    .setSound(defaultSoundUri)
                    .setContentIntent(pendingIntent)
                    .setPriority(NotificationCompat.PRIORITY_MAX)
                    .setDefaults(Notification.DEFAULT_ALL)
                    .addAction(R.drawable.ignore, "Snooze 1 hour", PendingIgnoreIntent);
    
    // Since android Oreo notification channel is needed.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        NotificationChannel channel = new NotificationChannel(channelId,
                "GarageWatcher Channel",
                NotificationManager.IMPORTANCE_HIGH);
        assert notificationManager != null;
        notificationManager.createNotificationChannel(channel);
    }

    assert notificationManager != null;
    notificationManager.notify(0, notificationBuilder.build());
}

Here, you’ll find that I created a static method (in case other activities needed it as well) that pops a notification with the highest available priority, so that the user should receive an audio cue as well as vibration if they haven’t muted their notifications completely.

Before I dive into each part of the above code, I want to emphasize how much I dislike it. The use of ‘dis’ to pass in the context is abhorrent both because it would likely be easier to simply use getApplicationContext() instead, but I am not familiar enough with Android’s Contexts and Activities to confidently play with this arrangement.

Otherwise, the code is straightforward and split into four sections (five, if you count the Oreo note): declare the Intent that should run when the user taps the notification, i.e., show the app screen; declare the Intent that should run when the notification’s ‘snooze’ button is tapped, i.e., send the device to sleep and remind the user after an hour; develop the notification and apply all necessary settings to it, such as the name, icon, and priority; and finally launch the notification with an ID that we can refer to later.

The next relevant code would then be sendIgnore.class, with which we will dismiss the notification and set up a timed event to remind the user that the door is still open:

public class sendIgnore extends Activity { 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        Intent ChirpIntent = new Intent(getApplicationContext(), wakeup.class);
        ChirpIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent PendingChirpIntent = PendingIntent.getActivity(getApplicationContext(), 
                0, ChirpIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        // send a snooze request to database
        FirebaseFunctions
                .getInstance()
                .getHttpsCallable("setSnooze")
                .call();

        long seconds = 60 * 60 * 1; // s / m / h
        AlarmManager.AlarmClockInfo info =
                new AlarmManager.AlarmClockInfo(
                        System.currentTimeMillis()+ (seconds * 1000),
                        PendingChirpIntent);

        assert alarmManager != null;
        alarmManager.setAlarmClock(info, PendingChirpIntent);
        NotificationManager manager = 
                (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        manager.cancel(0);
        finish();
    }
}

The first important section of this sendIgnore.onCreate is to manipulate the database via our Firebase Function setSnooze. Recall that this sets our device status to ‘awake’ = ‘false’ and ‘snooze’ = ‘true’. The device will then read that it should be snoozed, and enter deep sleep.

The next section sets up Android’s alarm system. Because we want this reminder to show up regardless of whether the app is actively running or not, and because the time period is relatively long, we’ll make use of the same alarms that wake you up in the morning. We’ll assign a new Intent, named ‘ChirpIntent’ here, to the alarm’s action, which simply updates the database and pops the main app open again.

There are a few options for how you can set an alarm for the future; due to various power-saving features built into Android, the different options vary in their accuracy. In our case, the reminder doesn’t necessarily have to match the time down to the second, so we’ll use the more general setAlarmClock() method.

Finally, after setting the alarm, we’ll dismiss the notification popup by referring to the same ID we gave it earlier – in this case, ‘0’. Calling finish() at the end of the onCreate method prevents the rest of the Activity class’ instantiation and destruction code from running, saving resources.

Notice that the ‘ChirpIntent’ calls yet another class, wakeup.class:

public class wakeup extends Activity { 
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // do stuff on the db to say not snoozing
        FirebaseDatabase database = FirebaseDatabase.getInstance();
        DatabaseReference snooze = database.getReference("snooze");
        DatabaseReference awake = database.getReference("awake");
        snooze.setValue("false");
        awake.setValue("true"); // fake an awake device 

        // cancel any alarms
        Intent ChirpIntent = new Intent(getApplicationContext(), wakeup.class);
        ChirpIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent PendingChirpIntent = PendingIntent.getActivity(getApplicationContext(), 
                0, ChirpIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        final AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        alarmManager.cancel(PendingChirpIntent);

        // then go to main page
        startActivity(new Intent(wakeup.this, MainActivity.class));
    }
}

Here, you see another instance of code that is very poor, but bear with me.

First, we grab a reference to a FirebaseDatabase instance, which lets us manipulate the values. Previously, our manipulation was all based around calling our Functions; this class demonstrates that it is also possible to edit the database values directly.

Then, you’ll see I re-instantiate ‘ChirpIntent’ in the exact same way as it was in the sendIgnore class. Yes, it would have been better to create a global variable and refer to it again. In any case, this is required in order to cancel any pending alarms. Android doesn’t give you many options in terms of creating and maintaining an alarm, so the only way to cancel it would be to provide it with a PendingIntent with the same ‘signature’.

Why would we need to cancel the alarm, if this alarm is what actually triggers the class in the first place? Well, it’s possible to activate this trigger without the alarm reaching its intended scheduled time. There are two conditions in which this can happen: the first being, if you tap the alarm notification in Android’s notification center – and the second being from our app’s MainActivity. Either way, we want to make sure the alarm doesn’t go off after we’ve manually cancelled it.

Finally, we get to build the app’s MainActivity.

Image 5

I arranged a couple TextView elements on a vertical LinearLayout as well as a couple of buttons that will serve as the user’s interface.

The status label, which reads ‘UNKNOWN’ for now, will read one of five messages during operation:

  • Loading’: before the app has an opportunity to authenticate and connect to the database to retrieve status information.
  • Door is Open’: any time the ‘awake’ value in the database is true. The only time ‘awake’ is true is immediately after the device has been turned on from the door opening, or when a snooze period is interrupted by the alarm, as described above.
  • Snoozing’: when ‘awake’ is false and ‘snooze’ is true. This occurs when the ‘snooze’ button is pressed, either on the popup notification or on the app’s MainActivity.
  • You closed it’: when both ‘awake’ and ‘snooze’ are false. This occurs only when the user has manually pressed the ‘I will close it’ button on the app.
  • Unknown’: when the database values for either ‘awake’ or ‘snooze’ are unrecognized. While it is highly unlikely anything should ever happen to the values that disrupt normal operation, it’s best to provide feedback to the user rather than just hiding the error behind a possibly-inaccurate state.

We’ll see how this is implemented in a moment.

When ‘snooze’ is active, the app’s snooze button disappears. The only way to dismiss the alarm is to press the ‘I will close it’ button or the alarm notification, both actions which urge the user to close the door (remember that dismissing the alarm also pops up the main page, which now should read ‘Door is Open’).

The code for our MainActivity looks something like this:

public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {...}
    protected void ConnectDatabase(){...}
    protected void SetStatus(){...}
}

I took the liberty of creating some easy-to-access references to the app’s visual elements, as well as instantiating a database reference and an authentication class, as variables of the MainActivity class:

FirebaseDatabase database = FirebaseDatabase.getInstance();
TextView txt_Status;
Button button_close;
Button button_snooze1;
LinearLayout snoozer;
String knownAwake = "unknown";
String knownSnooze = "unknown";
private FirebaseAuth mAuth;

These are assigned as necessary in the onCreate method. In addition, I also attach behaviors to the buttons here as well, which are omitted here since they’ve been described before:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //get auth for firebase
    mAuth = FirebaseAuth.getInstance();
    mAuth.signInAnonymously();

    final AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
    setContentView(R.layout.activity_main);
    txt_Status = findViewById(R.id.txt_status);
    txt_Status.setText("Loading...");
    button_close = findViewById(R.id.button_close);
    button_close.setOnClickListener(new View.OnClickListener(){
        public void onClick(View view) {
            // edits the firebase database to indicate the state is closed
            ...
            // cancel any alarms
            ...
        }
    });

    button_snooze1 = findViewById(R.id.button_snooze1);
    button_snooze1.setOnClickListener(new View.OnClickListener(){
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        public void onClick(View view) {
            // edits the firebase database to indicate snoozin
            ...
            // proactively clear any previous alarms j.i.c.
            alarmManager.cancel(PendingChirpIntent);
            // set new alarm
            alarmManager.setAlarmClock(info, PendingChirpIntent);
        }
    });

    snoozer = findViewById(R.id.snoozer); // the LinearLayout

    ConnectDatabase();
}

The last method call you see here is ConnectDatabase(), which takes advantage of a feature provided by FirebaseDatabase:

protected void ConnectDatabase(){
    DatabaseReference awake = database.getReference("awake");
    DatabaseReference snooze = database.getReference("snooze");
    awake.addValueEventListener(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            // This method is called once with the initial value and again
            // whenever data at this location is updated.
            String value = dataSnapshot.getValue(String.class);
            knownAwake = value;
            SetStatus();
        }
        @Override
        public void onCancelled(DatabaseError error) {
            // Failed to read value
            knownAwake = "false";
        }
    });

    snooze.addValueEventListener(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            String value = dataSnapshot.getValue(String.class);
            knownSnooze = value;
            SetStatus();
        }
        @Override
        public void onCancelled(DatabaseError error) {
            knownSnooze = "false";
        }
    });
}

The ValueEventListener lets us eliminate the necessity for a ‘refresh’ button or recurring event, since it is automatically raised whenever a value is changed on the database. By calling SetStatus() every time we receive this event, the app can always display the last known state. Of course, this is reliant on the listener to not disconnect – so adding a refresh button is possible. However, since this connection is created every time MainActivity is launched, simply restarting the app is sufficient to re-establish the connection via onCreate.

Finally, we have the method that updates the availability of the two buttons and the status text:

    protected void SetStatus(){

        if (knownAwake.equalsIgnoreCase("true")){
            txt_Status.setText("DOOR IS OPEN!");
            button_close.setVisibility(View.VISIBLE);
            snoozer.setVisibility(View.VISIBLE);
        } else if (knownAwake.equalsIgnoreCase("false")) {
            if (knownSnooze.equalsIgnoreCase("true")){
                txt_Status.setText("SNOOZING...");
                button_close.setVisibility(View.VISIBLE);
                snoozer.setVisibility(View.GONE);
            } else if (knownSnooze.equalsIgnoreCase("false")) {
                txt_Status.setText("You closed it.");
                button_close.setVisibility(View.GONE);
                snoozer.setVisibility(View.GONE);
            } else {
                txt_Status.setText("Unknown.");
                button_close.setVisibility(View.GONE);
                snoozer.setVisibility(View.GONE);
            }
        } else {
            txt_Status.setText("Unknown");
            button_close.setVisibility(View.GONE);
            snoozer.setVisibility(View.GONE);
        }
    }
}

Besides a little fenagling with Android’s interface builder – which is not unlike WPF, but it certainly doesn’t make things easy to arrange nicely – the app is now complete.

Conclusion

In this project, we’ve discovered the versatility of Firebase in creating a backend for our IoT needs. We’ve built a physical device that takes advantage of engineering constraints in the real world and translates that into a power-efficient sensor. Finally, we’ve built an Android app that starts on boot, receives and displays push notifications, and uses the alarm manager to set short-term reminders.

I hope this project has been as fun to follow along as it has been to pursue, and you’ll find something useful in here that can help you in developing a project of your own. I also hope you’ll pardon any rudimentary mistakes I’ve made here, and I welcome your feedback and suggestions. Stay safe and be well!

Afterword

I began developing the core idea behind this project because my father sometimes forgets to shut the garage door, especially if he’s bringing in a bunch of groceries or if something else is on his mind. This, of course, irritates my mother to no end, who treasures all the equipment we have stored in the garage as well. I happened to finish building the very last bit of the app in time for his birthday, as a gift to save him from getting another earful the next time he forgot to close the garage.

Over the course of coming up with this idea, I decided I wanted to explore serverless architectures. The inherent scalability, versatility, and reliability of running things solely in the cloud without requiring expensive VMs was alluring, but it’s not often I have a chance to dive into new technologies. The COVID-19 shutdown gave me the perfect opportunity to sit down and grind through hundreds of documents and tutorials – so if you’re in a similar position, I highly encourage you to also pursue a project that can widen your horizons, even a little bit.

And I have to admit – on multiple occasions throughout this project, I thought to myself: wouldn’t a server be easier to implement here? The answer is… it’s complicated. Yes – it’s easier to do, especially for a project scope as tiny as this was, but if you wanted to expand this service to more people, or you wanted to avoid downtime due to localized problems like power outages or *gasp* Windows Update, serverless does have its important niche to fill.

Thank you for reading, and coming on this head-spinning journey for me. I can’t wait to see what’s next in store.

History

  • 2nd June, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

g96b10
Engineer
United States United States
Eugene is a Multidisciplinary Engineer from Purdue University. His concentration is in Visual Design Engineering, with a minor in Product Life Management.
He is a hobbyist tinkerer with a love for combining artistic vision with engineering rigor. His multidisciplinary background means there's enjoyment to be found at all corners of a project.

Comments and Discussions

 
QuestionNice work Pin
Mike Hankey10-Jul-20 5:58
professionalMike Hankey10-Jul-20 5:58 
GeneralInteresting project that could be extended to other status notifications Pin
Richard Chambers5-Jul-20 23:42
MemberRichard Chambers5-Jul-20 23:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 2 Jun 2020

Stats

3.8K views
9 bookmarked