Austin White
Austin's Portfolio

Austin's Portfolio

Snail Mail... with a hint of WIFI

Snail Mail... with a hint of WIFI

Austin White's photo
Austin White

Published on Jan 27, 2021

14 min read

TDLR: I built an IoT motion detector and stuck it to the inside of my mailbox. Now I get an alert whenever mail is delivered Did I mention, it can last approx. 600 days on one charge... Spoilers.

IMG_20201222_173202854_HDR.jpg

Background

As a programmer, a constant lure exists. It's always there, sneaking up on me. In some ways, it's like a siren luring sailors to their death. In this case, the beautiful siren is automation and it's tempting me to the death of my free time.

First off, the "problem". The mailbox for my apartment is in an odd place, and I never check it. What? A programmer being lazy? No. The simplest solution is I just check it more often but what fun is that, none. I have plenty of background experience with wifi-enabled microcontrollers and a motion processing unit. Heck, I'll slap something together in a few days. RIP free time.

Hardware

The outside appearance is a bland black box with a label alerting anyone to what it is (to prevent panic).

dyno.gif PSA: Label your DIY devices. It may be a cool gadget to you but to others, it's a mess of wires with blinking lights.

Esp8266

This is a wifi-enabled microcontroller that is supported by the Arduino platform. This makes life much easier. It's got gpio, low power deep sleep, and wifi.

MPU6050

This thing is cool. I mean really cool. This sensor measures acceleration and angles, using an accelerometer and gyroscope but that isn't the cool part. It has a "Digital Motion Processing" Unit. Basically, it has it's own brain on board that can take some of the weight off the Esp8266 while using less power.

Wiring

The Esp8266 connects to the MPU6050 via I2C. In this case, ESP pins 1, 2 are connected to the MPU's SDA and SCL lines. The power is connected (3.3v and Ground). Last but not least, an interrupt pin. This the MPU's simplistic and lower power way of it telling the ESP that something happened (new data, motion detected, etc. User programmed). It's connected to the ESP's pin 8.

All pins for the ESP8266 are Wemos D1 mini mappings. For example, in code, pin 7 is actually 13. Here is a table for conversion: chewett.co.uk/blog/1066/pin-numbering-for-w..

The MPU interrupt pin will be connected to the ESP's reset pin after debugging and testing is complete. This will wake up the ESP from a deep sleep.

The death of my free time

The MPU6050 interrupt pin default state(High or Low) can be programmed at power on. Unlike the MPU6050, the Esp8266's reset pin hardware-based and requires the reset pin to be pulled low momentarily to reset the board.

Unknown to me, the MPU6050 defaults and initialization sequence cause the interrupt pin to send an interrupt. In short, there is an endless loop of the Esp8266 starting, initializing the MPU6050, then resetting the Esp8266 immediately before I want.

The correct sequence is:

  1. Esp8266 powers on
  2. Send API call to trigger mail alert
  3. Initialize MPU6050
  4. Set motion trigger
  5. Enter deep sleep
  6. Motion Reset Esp8266 (repeat from step 1)

What is happening:

  1. Esp8266 powers on
  2. Send API call to trigger mail alert
  3. Initialize MPU6050
  4. RESET

This is bad, my solution is to find a way to ignore the MPU6050 "startup" interrupt but how?

First idea, software-based? Quick google search Esp8266 ignore reset pin, no luck.

Second, hardware...

I could have a second low power microcontroller(maybe an ATTiny85) handle and filter the MPU6050. While this would work, it would increase power consumption, design complexity, and overall just not fun :(.

Light Bulb!

Here is a patch I came up with that uses an NPN transistor. The Esp8266 reset pin just needs to be held high, so let's give it just that.

The 3.3v pin is connected and the reset pin are connected to a NPN transistor so when the base pin is triggered, the reset pin is flooded with 3.3 volts. Hopefully, this will drown out the low signal from the MPU6050 interrupt pin.

It wasn't but... then I remembered electricity takes the least past of resistance (Thank you high school physics!). So, to make the low signal less appetizing to both the 3.3v and reset pin, I put a small resistor between the reset pin and the MPU6050 interrupt pin. Now when programming the MPU6050, the NPN transistor is closed, blocking any interrupts. Afterward, the NPN transistor is opened again, allowing planned operation.

It worked! However, I will change this when the PNP transistors arrive in the mail.

Labotimy

A few changes can be made to the MPU-6050 development board GY-521 to decrease power usage.

Instructions

Final circuit design

202101262329121000.jpg

Software

This part should be simple, right? Connect to the internet, send an alert (REST API call), setup the MPU6050 and its motion trigger, go into a deep sleep(wake up will start at the beginning of this logic), repeat.

MPU6050

Bye-bye, free time. Numerous sticking points here. First off, was importing and getting all the libraries to behave. Missing puzzle piece, find and install the I2CDEV library. Done.

Next up, nothing. There is no response from the MPU6050, 3-4 hours double-checking wiring and pins. It was a combination of problems. Esp8266 pins are differently mapped than labels in the dev board variant (Wemos d1 mini). Secondly, I2C requires manual initialization for the ESP8266 with Wire.begin().

Link to key/mapping: chewett.co.uk/blog/1066/pin-numbering-for-w..

The board crashes each time with a runtime error. Surprisingly, the Esp8266 handles interrupts differently than other boards. This requires adding ICACHE_RAM_ATTR right before the name of the interrupt function.

Here is where I found the solution: esp8266.com/viewtopic.php?f=11&t=20118&..

Let's get fancy!

I borrowed some example code to add a wifi manager (no hardcoded credentials or URLs).

Code

#include <FS.h>
#include <arduino_mpu.h>
#include <inv_mpu.h>
#include <inv_mpu_dmp_motion_driver.h>
#include <Wire.h>
#include <DNSServer.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

#define PL(x) Serial.println(x)
#define P(x) Serial.print(x)

char* host = "";
const int httpsPort = 443;
char api_path[100];

//flag for saving data
bool shouldSaveConfig = false;

// Use web browser to view and copy
// SHA1 fingerprint of the certificate
char fingerprint[] = "";


//callback notifying us of the need to save config
void saveConfigCallback () {
 Serial.println("Should save config");
  shouldSaveConfig = true;
}

void ICACHE_RAM_ATTR interrupt() {
  PL("Waaaaaa?");
}

void sendAlert() {
  // Use WiFiClientSecure class to create TLS connection
  WiFiClientSecure client;
 Serial.print("connecting to ");
 Serial.println(host);

 Serial.printf("Using fingerprint '%s'\n", fingerprint);
  client.setFingerprint(fingerprint);

  if (!client.connect(host, httpsPort)) {
   Serial.println("connection failed");
    return;
  }

  String url(api_path);
 Serial.print("requesting URL: ");
 Serial.println(url);

  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "User-Agent: BuildFailureDetectorESP8266\r\n" +
               "Connection: close\r\n\r\n");

 Serial.println("request sent");
  while (client.connected()) {
    String line = client.readStringUntil('\n');
    if (line == "\r") {
     Serial.println("headers received");
      break;
    }
  }
  String line = client.readStringUntil('\n');
  if (line.startsWith("{\"state\":\"success\"")) {
    Serial.println("esp8266/Arduino CI successfull!");
  } else {
    Serial.println("esp8266/Arduino CI has failed");
  }
  Serial.println("reply was:");
  Serial.println("==========");
  Serial.println(line);
  Serial.println("==========");
  Serial.println("closing connection");
}


void setup() {

  // Ignore mpu6050 interrupt signal while init of module
  pinMode(15, OUTPUT);
  digitalWrite(15, HIGH);

  // Debug
  Serial.begin(115200);
  while (!Serial);


 Serial.println("mounting FS...");

  if (SPIFFS.begin()) {
   Serial.println("mounted file system");

    if (SPIFFS.exists("/config.json")) {

      //file exists, reading and loading
     Serial.println("reading config file");
      File configFile = SPIFFS.open("/config.json", "r");

      if (configFile) {
       Serial.println("opened config file");
        size_t size = configFile.size();
        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> buf(new char[size]);

        configFile.readBytes(buf.get(), size);

        DynamicJsonDocument json(1024);
        auto deserializeError = deserializeJson(json, buf.get());
        serializeJson(json, Serial);
        if ( ! deserializeError ) {
         Serial.println("\nparsed json");
          strcpy(host, json["host"]);
          strcpy(api_path, json["api_path"]);
          strcpy(fingerprint, json["fingerprint"]);
        } else {
         Serial.println("failed to load json config");
        }
        configFile.close();
      }
    }
  } else {
   Serial.println("failed to mount FS");
  }
  //end read




  // The extra parameters to be configured (can be either global or just in the setup)
  // After connecting, parameter.getValue() will get you the configured value
  // id/name placeholder/prompt default length
  WiFiManagerParameter custom_host("host", "api.github.com", host, 100);
  WiFiManagerParameter custom_path("path", "/api/2v/request_here", api_path, 100);
  WiFiManagerParameter custom_fingerprint("fingerprint", "hosts sha1 finger print, found under ssl cert.", fingerprint, 100);

  WiFi.mode(WIFI_STA);

  WiFiManager wm;
  //set config save notify callback
  wm.setSaveConfigCallback(saveConfigCallback);


  //add all your parameters here
  wm.addParameter(&custom_host);
  wm.addParameter(&custom_path);
  wm.addParameter(&custom_fingerprint);



  bool res;

  res = wm.autoConnect("snailMailAlert", "tangoWhiskey");

  if (!res) {
   Serial.println("Failed to connect");
    // ESP.restart();
  }
  else {
    //if you get here you have connected to the WiFi
   Serial.println("connected...yeey :)");
    //read updated parameters
       Serial.println("connected...yeey 1:)");

    strcpy(host, custom_host.getValue());
       Serial.println("connected...yeey :2)");

    strcpy(api_path, custom_path.getValue());
       Serial.println("connected...yeey :3)");

    strcpy(fingerprint, custom_fingerprint.getValue());
       Serial.println("connected...yeey :4)");


    if (shouldSaveConfig) {
     Serial.println("saving config");
      DynamicJsonDocument json(1024);

      json["host"] = host;
      json["api_path"] = api_path;
      json["fingerprint"] = fingerprint;

      File configFile = SPIFFS.open("/config.json", "w");
      if (!configFile) {
       Serial.println("failed to open config file for writing");
      }

      serializeJson(json, Serial);
      serializeJson(json, configFile);

      configFile.close();
      //end save
    }
  }

  // Alert user motion detected
  sendAlert();



  Wire.begin();         // I2C

  // mpu6050 init
  struct int_param_s params;
  params.pin = 13;
  params.cb = interrupt;
  PL(mpu_init(&params));

  // mpu6050 interrupt
  PL(mpu_set_int_level(1));

  // set motion trigger
  PL(mpu_lp_motion_interrupt(32, 5, 5));

  PL("Ready...");
  delay(20);
  digitalWrite(15, LOW);   // turn the LED on (HIGH is the voltage level)
  delay(20);


  // Deep sleep, seconds does not matter as wake up pin not connected to itself
  ESP.deepSleep(10E6, WAKE_RF_DEFAULT);
}


void loop() {
  // Nothing to see here... 101010
}

Power consumption

Esp8266 in deep sleep: 20uA per hour

MPU6050 in sleep: 5uA per hour

Tolerance: 15 uA per hour

Total: 40uA = 0.04 milliamps per hour

AA batteries have 800 milliamps total

0.04 *24 = 0.96 milliamps per day

800 / 0.96 = 833 days in sleep mode

Esp8266 in operations: 150 approx. milliamps per hour

MPU6050 in operation: 5 milliamps per hour

Total: 155 milliamps per hour

Time per alert/wake up: 5 seconds

155 / 60 = 2.583 milliamps per minute

2.583 / 60 = 0.04 milliamps per second

0.04 * 5 = 0.2 milliamps per wake up

If mail is delivered 5 days out of 7 (worst case), then 5 wakes ups per week and 20 per month. Also, 240 wake ups per year.

0.2 * 240 = 48 milliamps per year for wake-ups

So, the math checks out. AA batteries will last more than a year.

The end

Thank you for reading till the end. I am currently looking for an internship in computer science or a related field. If you or someone you know can help, please contact me on Linkedin: linkedin.bltstack.dev.

Bye!

 
Share this