# IOT mailbox status module with LoPy4 ###### tags: iot mailbox lopy4 ![](https://i.imgur.com/r08gGfY.jpg) By Joel Sörensson (js226cq) This project explains how to build a mailbox module that is connected to the internet and informs you if your physical mailbox is empty or if you got mail! :mailbox: The module senses when the mailbox is opened and then checks if there is content inside. :brain: Furthermore, together with the cloud platform it logs when the mail arrives or is emptied. Created using LoPy4 by Pycom with MicroPython. Estimated time: ~8-12 hours if you have all the material available to follow this tutorial. ## Objective At my job, the mailbox is located 25m away from the building and out of sight. Recently, the mail delivery schedule changed to every other day, instead of every day. The mail is usually important to us and our customers, so it should preferably be picked up and put up in the store as soon as it arrives. However, the mail schedule and location of the mailbox makes it easy to forget. As well as leaving the store to check the mailbox, not knowing if any mail has arrived feels inefficient. I've felt like there must be a way to remove these problems. :email: :arrow_right: :mailbox: That is how this project was thought of and why I decided to create a cloud connected module for the mailbox that can inform us about its physical status! ## Material | Hardware | Vendors | Price | | ----------------- |:-----------|:-------| | Pycom LoPy4 | pycom.io | 35 € | | Pycom Expansion board v3.1 | pycom.io | 16 € | | Ultrasonic sensor HC-SR04 | m.nu | 4 € | | Photo transistor light sensor or Photocell | adafruit.com or m.nu | 1-2 € | | Power bank | amazon.se | 8 € | | Breadboard | m.nu | 4 € | | Wires | m.nu (40pcs) | 4 €| | Resistor - 10k ohm | m.nu (600pcs) | 8 €| | Micro usb cable | | - | In the IoT mailbox module I use the Pycom LoPy4, which is a microcontroller that is well suited for an IoT project. It has multiple bands of connectivity and is programmed by MicroPython. However, in this particular project you can probably use any Pycom device that has a Wifi capabilities. <img src="https://i.imgur.com/iVPOZPM.jpg" alt="drawing" width="400"/> The Expansion Board provides easier access to the pin connections on the LoPy4 used for input or output. It also has USB-connection for code transfer and power. Furthermore, it provides a 2-pin JST-connection for compatible batteries, which can be recharged through the expansion board (not used in this project). <img src="https://i.imgur.com/VnJIw7F.jpg" alt="drawing" width="300"/> The HC-SR04 ultrasonic sensor uses sonar to determine distance to an object like bats do. It offers excellent range accuracy and stable readings in an easy-to-use package. The measuring range is 2 – 400cm and it has an accuracy of 3mm. In this project it determines if there's mail in the mailbox through a distance change inside. Requires 5v power! <img src="https://i.imgur.com/c5TM9FY.jpg" alt="drawing" width="300"/> The Photo Transistor Light Sensor is a simple sensor that detects ambient light. It's kind of the opposite of an LED - when light hits the little chip inside, it induces current to flow from the long pin to the short pin. I had this sensor available at home, but you can also use a Photocell a.k.a Light Dependant Resistor (LDR) which should work as well! <img src="https://i.imgur.com/yG7MT9S.png" alt="drawing" width="100"/> OR<img src="https://i.imgur.com/7j6zL4h.jpg" alt="drawing" width="200"/> I had a power bank with an on/off button available at home so that is what I used for powering the module. I recommend to find a really short micro USB cable if you want to package the power bank in the module neatly! <img src="https://i.imgur.com/OtD75st.jpg" alt="drawing" width="200"/> The breadboad is very useful when prototyping the hardware setup, to easily connect the different pins and get an overview. <img src="https://i.imgur.com/IOBtkel.jpg" alt="drawing" width="200"/> Jumper wires, a 10k ohm resistor and a USB cable was further used to complete the project hardware-wise. <img src="https://i.imgur.com/ObUWSzj.jpg" alt="drawing" width="200"/><img src="https://i.imgur.com/VTDHKsT.png" width="200"/> <img src="https://media.startech.com/cms/products/main/uusbhaub6in.main.jpg" width="200"/> ## Computer setup When getting started it is recommended that you **update the firmware** on your **LoPy4** and as well as the **Expansion Board**. So grab those and connect your micro USB cable, then follow **Pycoms guides** on how to do this below: * [Updating Pycom device firmware](https://docs.pycom.io/updatefirmware/device/) * [Updating Pycom Expansion Board firmware](https://docs.pycom.io/updatefirmware/expansionboard/) I'm using **Visual Studio Code** to write code and communicate with the device over USB, together with the plugin **Pymakr** which requires **Node.js**, so you need to install that too. * [Visual Studio Code](https://code.visualstudio.com/) * [Node.js](https://https://nodejs.org/en/) * [Pymakr](https://marketplace.visualstudio.com/items?itemName=pycom.Pymakr) For a complete guide on the setup, see Pycom’s [tutorial](https://docs.pycom.io/gettingstarted/). There you'll learn how to create your first project with Pymakr. ## Putting everything together Let's start building this thing, if you use a different sensor model than mine, please make adjustments according to the datasheet and documentation. It's actually easier than it seems at first. First things first, be certain that the breadboard or the pycom device is not connected to power while connecting the sensors, they can take damage if put directly onto power. Let's start with connecting **VIN (5v)** to red line (+) on the breadboard and **GND** to black line (-). Then we'll continue with the **light sensor**. We read analog data from the sensor on Pin 18. Connect according to image below: ![](https://i.imgur.com/Ud8HZIj.jpg) We will move on to the HC-SR04 ultrasonic sensor. The ultrasonic **trigger** we'll connect to **Pin 20** and **echo** to **Pin 21**. Connect according to image below: <img src="https://i.imgur.com/YM4pEmf.png" width="600"/> The HC-SR04 sensor model requires 5v to give reliable readings, have that in mind when further connecting other potential power supplies than your computers usb port. I later use a powerbank that delivers 5v to the expansion boards micro USB port, which works well. #### Final look of the wiring on a breadboard: ![](https://i.imgur.com/JTt1ny4.jpg) #### Final look of the wiring directly to expansion board: ![](https://i.imgur.com/KddG3TD.jpg) ## The platform The platform we'll be using is Ubidots. It supports Pycom devices very well and is free* with a [STEM account](https://ubidots.com/stem/). Further, it's a cloud-based and relatively easy-to-use platform. So begin with signing up at [Ubidots](https://ubidots.com/stem/)! Ubidots provides a beginner-friendly interface for presenting our data on their dashboard, which is the main reason we use it. Their free tier has a limit of retaining data for 30 days, however in my case that works well enough. **More than three devices and advanced functionality are paid upgrades.* ## The code By now you should have been able to connect and test running some code on your LoPy4. If not, head to [Pycoms getting started tutorial](https://docs.pycom.io/gettingstarted/) before proceeding with the coding. #### The file structure: ```javascript PROJECT lib - keys.py - ubi.py - urequests.py boot.py main.py ``` #### The libraries: Create a file in the library called ``` keys.py ``` where you can import your different passwords and tokens from. Into the file, copy and paste this: ```javascript=1 ubidots_token = "insert your ubidots-token" ssid = "your wi-fi name here" wpa = "your wi-fi password" ``` Replace the "example text" in the code with your specific information. Here is a guide on how to find your Ubidots TOKEN: * [How to find your Ubidots token](https://help.ubidots.com/en/articles/590078-find-your-token-from-your-ubidots-account) Then create a file called ```urequests.py``` and paste this code in to it: ```javascript=1 import usocket class Response: def __init__(self, f): self.raw = f self.encoding = "utf-8" self._cached = None def close(self): if self.raw: self.raw.close() self.raw = None self._cached = None @property def content(self): if self._cached is None: try: self._cached = self.raw.read() finally: self.raw.close() self.raw = None return self._cached @property def text(self): return str(self.content, self.encoding) def json(self): import ujson return ujson.loads(self.content) def request(method, url, data=None, json=None, headers={}, stream=None): try: proto, dummy, host, path = url.split("/", 3) except ValueError: proto, dummy, host = url.split("/", 2) path = "" if proto == "http:": port = 80 elif proto == "https:": import ussl port = 443 else: raise ValueError("Unsupported protocol: " + proto) if ":" in host: host, port = host.split(":", 1) port = int(port) try: ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM) except: ai = usocket.getaddrinfo(host, port) s = usocket.socket() ai = ai[0] try: s.connect(ai[-1]) if proto == "https:": s = ussl.wrap_socket(s, server_hostname=host) s.write(b"%s /%s HTTP/1.0\r\n" % (method, path)) if not "Host" in headers: s.write(b"Host: %s\r\n" % host) # Iterate over keys to avoid tuple alloc for k in headers: s.write(k) s.write(b": ") s.write(headers[k]) s.write(b"\r\n") if json is not None: assert data is None import ujson data = ujson.dumps(json) s.write(b"Content-Type: application/json\r\n") if data: s.write(b"Content-Length: %d\r\n" % len(data)) s.write(b"\r\n") if data: s.write(data) l = s.readline() #print(l) l = l.split(None, 2) status = int(l[1]) reason = "" if len(l) > 2: reason = l[2].rstrip() while True: l = s.readline() if not l or l == b"\r\n": break #print(l) if l.startswith(b"Transfer-Encoding:"): if b"chunked" in l: raise ValueError("Unsupported " + l) elif l.startswith(b"Location:") and not 200 <= status <= 299: raise NotImplementedError("Redirects not yet supported") except OSError: s.close() raise resp = Response(s) resp.status_code = status resp.reason = reason return resp def head(url, **kw): return request("HEAD", url, **kw) def get(url, **kw): return request("GET", url, **kw) def post(url, **kw): return request("POST", url, **kw) def put(url, **kw): return request("PUT", url, **kw) def patch(url, **kw): return request("PATCH", url, **kw) def delete(url, **kw): return request("DELETE", url, **kw) ``` The last of the libraries is ```ubi.py```. Here you can add variables you might want to send to ubidots. On line 26-28 you can edit the labels on the variables you are sending. ```javascript=1 import urequests as requests import keys TOKEN = keys.ubidots_token #Import your key from key file # Builds the json to send the request def build_json(variable1, value1, variable2, value2, variable3, value3): try: data = {variable1: {"value": value1}, variable2: {"value": value2}, variable3: {"value": value3}} return data except: return None # Sends the request. Please reference the REST API reference https://ubidots.com/docs/api/ def post_var(device, value1, value2, value3): try: url = "https://industrial.api.ubidots.com/" url = url + "api/v1.6/devices/" + device headers = {"X-Auth-Token": TOKEN, "Content-Type": "application/json"} # Edit the labels here to your liking data = build_json( "Mailbox status", value1, "Distance (cm)", value2, "Light level", value3 ) if data is not None: print(data) req = requests.post(url=url, headers=headers, json=data) return req.json() else: pass except: pass ``` #### Boot and main: Let's be honest right here, the following code is messy, I just hope you can understand it. I'm not a programmer, but at least I managed to get it to work! ```boot.py``` ```javascript=1 # boot.py -- run on boot-up from machine import UART import machine import keys from network import WLAN import os uart = UART(0, baudrate=115200) os.dupterm(uart) machine.main('main.py') wlan = WLAN(mode=WLAN.STA) nets = wlan.scan() for net in nets: if net.ssid == keys.ssid: print('Found the network! Trying to connect..') wlan.connect(net.ssid, auth=(net.sec, keys.wpa), timeout=5000) while not wlan.isconnected(): machine.idle() # power saving while waiting print('WLAN connection succeeded!') break exec(open("main.py").read()) # To be sure we run main ``` In ```main.py``` we create a loop that checks the light value. An open mailbox is >5 in analog light value in my case and the distance inside my mailbox when empty is 8.6cm. You need to test this and edit the code for your specific values. When the content is too close to the ultrasonic sensor (<2cm), it reads 1195.0, which is an invalid reading in this case. But this is taken into account in the code by having both a >X and <X value that needs to be fulfilled in both full/empty cases. ```main.py``` ```javascript=1 import utime import machine import pycom import ubi from machine import ADC from machine import Pin LightSensorPin = 'P18' # sensor connected to P18. lightPin = Pin(LightSensorPin, mode=Pin.IN) # set up pin mode to input adc = ADC(bits=10) # create an ADC object bits=10 means range 0-1024 the lower value the less light detected apin = adc.channel(attn=ADC.ATTN_11DB, pin=LightSensorPin) # create an analog pin on P16; attn=ADC.ATTN_11DB measures voltage from 0.1 to 3.3v light = apin() # read analog value from Light sensor count = 0 i = 0 while True: print("Light", light) if light > 5: # If light > 5 initialise Ultrasonic Sensor pins. You light level when mailbox open needs to be calibrated for your case. i = 1 # i=1 means the mailbox has been opened and we can start looking for mail count = 0 # sets the count to 0, so that we can send mailbox data 10s after mailbox has been closed. REMOVE THIS ONE TO RECEIVE LIGHT CALIBRATION VALUES IN UBIDOTS. trigger = Pin(Pin.exp_board.G7, mode=Pin.OUT) # Lopy4 specific: Pin('P20', mode=Pin.IN) echo = Pin(Pin.exp_board.G8, mode=Pin.IN) # Lopy4 specific Pin('P21', mode=Pin.IN) trigger(0) # Ultrasonic distance measurment def distance_measure(): # trigger pulse LOW for .02 (just in case) trigger(0) utime.sleep(0.02) # trigger HIGH for a .1s pulse trigger(1) utime.sleep(0.1) trigger(0) # wait for the rising edge of the echo then start timer while echo() == 0: pass start = utime.ticks_us() # wait for end of echo pulse then stop timer while echo() == 1: pass finish = utime.ticks_us() # calculate distance by using time difference between start and stop # speed of sound 340m/s or .034cm/us. Time * .034cm/us = Distance sound travelled there and back # divide by two for distance to object detected. distance = ((utime.ticks_diff(start, finish)) * .034)/2 return distance * -1 print("Distance", distance_measure()) # send updated mailbox info to ubidots if i == 1: # my measured distance when mailbox is empty = 8.6, when less than that = we got mail! Needs to be calibrated for your case # distance_measure > 900 is when the content is too close to the sensor for it to make valid readings (gives 1195) if distance_measure() < 8 and i == 1 and count == 10 or distance_measure() > 900 and i == 1 and count == 10: mailbox = 2 i = 0 count = 0 ubi.post_var("Mailbox", mailbox, distance_measure(), light) print("Updating ubidots to mail in mailbox...") if distance_measure() > 8 and i == 1 and count == 10 or distance_measure() < 900 and i == 1 and count == 10: mailbox = 1 i = 0 count = 0 ubi.post_var("Mailbox", mailbox, distance_measure(), light) print("Updating ubidots to empty mailbox...") utime.sleep(1) light = apin() count += 1 ``` #### Short explanation of how ```main.py``` works: If the light value is >5 it means that the mailbox has been opened. This triggers the ultrasonic sensor, which looks at the distance inside as long as the mailbox is open. When the mailbox is closed again, the last full/empty mailbox data is sent to ubidots. The variable Mailbox = 1 when empty and = 2 when full. This was required in ubidots for visualizing on/off, since 0 and 1 was apparently not valid. The other variables that is sent is the light value and the distance inside. These can be viewed in ubidots to calibrate your specific light and distance limits inside the mailbox, without the need of keeping the LoPy4 tethered to your computer. However, to do this, you need remove count = 0 inside the first if loop (look for comment inside code). ## Transmitting the data / connectivity Since I don't have LoRaWAN or Sigfox available at my current location, we use Wifi in this project. Unfortunately, the mailbox is just outside my jobs Wifi range. However, it is next to a shed that has electricity, so for my project I've put a mobile Wifi-router in the shed until I can use LoRaWan or Sigfox. The data is packaged through a JSON-object which is sent by HTTP pushes (webhooks) to the Ubidots API about 10 seconds after the mailbox has been filled or emptied. To elaborate, an open mailbox triggers distance measurement and a closed mailbox triggers data push after ∼10s (once, until opened again). ![](https://i.imgur.com/wDHDE3Z.jpg) ## Presenting the data The data I wanted from this device was to be able to see if the mailbox has content or not. I can now see this in the Ubidots dashboard with the mailbox-status dot as you can see on the left on the images below. Red is empty (variable Mailbox=1) and green is full (variable Mailbox=2). As mentioned before, Ubidots could not use 0 and 1 as values for some reason in this visualization, which is why I use 1 and 2. Furthermore, I can now also log and see around what time previous mail arrivals has been on specific days with the bar chart. From this I can estimate around what time the mail will arrive on specific days in the future. ![](https://i.imgur.com/RX54Bam.png) ![](https://i.imgur.com/35nbESf.png) As seen below, under the mailbox device tab I can see what values the sensors measured during the last data uploads. As mentioned earlier, the other variables that is sent is the light value and the distance inside. You can use this functionality to find which specific limit values you should set for your mailbox, as a calibration without having to bring your computer and a cable sticking out. But again, to do this, you need remove count = 0 inside the first if loop (look for comment inside code in ```main.py```). ![](https://i.imgur.com/ciaL1P5.jpg) There is a 30 day limit to the data preservation on ubidots free version, in this project it works well. However, for future visualizations, it would be interesting to have a mean value for mail arrival on specific days, I guess the more data the better in that case. ## Finalizing the design This project went relatively well and without any big problems. The most tricky part was figuring out how to send the data to the cloud. Luckily, the web is full of examples. For optimization, this project could benefit from a more sensitive light sensor, if picking up the mail late in the evening (dark). As well as a smaller packaging that is angled at the top, so that the mail can slide down in front of the sensor, with no possibility to get stuck on top of the module. As mentioned before, I've set up a mobile wifi router to be able to reach the internet. This would be an even more realisticly working product in an area with LoRaWan or Sigfox reach. As an end note, this was a really fun and manageable project! Learned a lot! Finalization pictures below! ![](https://i.imgur.com/7MdVlUF.jpg) ![](https://i.imgur.com/jkjzSgk.jpg) ![](https://i.imgur.com/pwxg23z.jpg) ![](https://i.imgur.com/6DEeEN7.jpg) ![](https://i.imgur.com/7X4THEc.jpg) ![](https://i.imgur.com/r08gGfY.jpg) Thank you for a fun course!!