Using Radio Frequency and Python to control the World

Michael K
Level Up Coding
Published in
8 min readDec 12, 2022

--

Three boards are displayed next to each other: An Adafruit Feather M0 RFM9x, a Adafruit Ultimate GPS module, and finally an Adafruit Feather M0 RFM9x with an Adafruit Power Relay FeatherWing attached on top.

I’ll save the World for later but figured I’d start with something smaller first — my garage. Our garage door is incredibly slow, so even if we trigger it from a few homes down the street we still end up having to wait in the driveway. I’ve been seeing a few LoRa projects and it seemed like a good fit to fix this issue as we could communicate with a device at my home even when not in range of my WiFi network (and without using cellular data).

In this article, I’d like to go over how I built a system to remotely open my garage door as soon as I pull into my neighborhood. This way, it would be open as we pulled up and we wouldn’t have to wait. With such an important problem to solve, let’s get started!

LoRa

LoRa (from Long Range) uses low-power radio communication to transmit data using three frequencies: 433 MHz, 868 MHz, and 915 MHz. Which frequency you should use ultimately depends on your country (and region sometimes). The Things Network has a great list aggregated but you should still check your local laws to be sure — unless you like random knocks at the door from your local friendly government agency. I’ll be using a 915 MHz radio since I’m based in the US and there are no additional restrictions when compared to using 433 MHz.

During testing, I was able to achieve ranges of 215 meters (700 feet) using a basic antenna so with a more advanced antenna you could get more range — depending on your environment ultimately. If you have a line of sight between the antennas, your range is increased even more.

Radio Frequency

RF is all around us daily — your WiFi, wireless mice/keyboards, Bluetooth, GPS, and so much more. However, they all operate on different frequencies depending on the application (and throughput required). The great thing about LoRa using a lower UHF frequency is that we will get pretty good range with it even with a basic antenna when compared to WiFi or Bluetooth. The downside to this, however, is we are quite limited in the amount of data we can send per packet.

Chart of radio frequencies and the common use cases
Radio Frequency bands graphic via TeraSense

Communication limitations

Since LoRa isn’t a direct communication protocol, any data we send could be seen by other radios in range. There’s no way only to send data to a specific listener (unless you are using LoRaWAN) so we have to keep in this mind when building the solution. One workaround for this is just to encrypt the data we’re sending, this way only those with the encryption keys can decipher what we’re sending, for example:

Encrypted: 240e805f37511b9ea82911de60775c623024a2730125f12805500b94
Decrypted: {"channel": 1, "message": "..."}

However, not all microcontrollers support encryption. In my case, this ended up being a limiting factor and made me remove encryption from my workflow. But this is something I will keep in mind for the future as I thought it was an interesting approach and guarantees no third party is snooping.

LoRaWAN

LoRaWAN is a networking protocol on top of LoRa. Nodes connect to gateways which act as a bridge allowing the node to send/receive packets from the network at large versus only ones in range. If you were building an entire smart home around LoRa or wanting to incorporate multiple devices, going with LoRaWAN is the right move. Since the project I have in mind is only using two nodes, I can get away with just using LoRa — but I already see how I could take advantage of LoRaWAN for some future project ideas.

Hardware

To power the project, I picked up two Adafruit Feather M0s that have an RFM95 radio pre-attached. These radios are what are called ‘transceivers’, meaning that they can both transmit and receive radio waves — exactly what we need as we want to pass data from one radio to another.

Adafruit’s Feather lineup is a series of microcontrollers and companion boards, called FeatherWings, to enable many different use cases and form factors. You could also use multiple wings with one Feather device which is super useful.

An Adafruit Feather M0 RFM9x
Adafruit Feather M0 RFM9x

The CircuitPython build and microcontroller powering this device, the SAMD21, is quite limited though. It cannot handle long integers, has no hardware source of random, no out-of-the-box JSON support, and has only 32KB of RAM. While this does seem like enough at first, just after CircuitPython has loaded alone, we are already down to only 17KB of memory left. Whichever Feather is handling the GPS module will end up with only 500 bytes to 1 KB of RAM available after loading all of the libraries and parsing some GPS data.

Luckily, this ended up being just enough to accomplish what I needed but I will cover this again in the conclusion.

Garage Hardware

To open the garage door, I’m going to use an Adafruit Power Relay FeatherWing hooked up to one of the Feathers. For my garage door system, I have a hardwired button that we can press to open/close the door which runs to the back of the door motor to two screw terminals.

The back of a garage door motor is shown. Three screw  terminals are shown: “Push Button”, “COM”, “Safety Beam”.
Back of my garage door motor

Using a multimeter, I saw there was no voltage across the pins (AC or DC) and when checking in continuity mode, I can see that the button just connects the two pins when depressed. This makes our job wiring super easy! We’ll wire up the COM ports together and then connect a wire from NO (normally open — or not connected by default) to the top terminal used for the door opener. This way we don’t open the door as the system powers on after a power loss for example.

A wiring diagram is shown with an Adafruit Power Relay FeatherWing and a three-post terminal to the side. A wire is hooked from “COM” on the FeatherWing to the “COM” port on the other terminal. Another wire is running from “NO” to the “Push Button” port on the other terminal.
Wiring diagram

On the back of the relay board, we can solder a jumper pad to tie the signal line to a specific pin on the Feather. Then all we have to do is set that pin’s output to high to trigger the relay:

import board
import digitalio
import time

# Setup the pin
relay = digitalio.DigitalInOut(board.A0)
relay.direction = digitalio.Direction.OUTPUT

# Switch the relay to NO (Normally Open)
relay.value = True

# Slight delay to simulate pressing the button momentarily
time.sleep(0.1)

# Switch the relay back to NC (Normally Closed)
relay.value = False

Car Hardware

To track my vehicle’s position, I grabbed an Adafruit Ultimate GPS FeatherWing for the other Feather. It’ll communicate over UART with the Feather and allow us to see our car’s position, speed, altitude, and more if desired. Adafruit has also released a library to make working with the GPS module as easy as possible, allowing us to easily fetch our coordinates in a few short commands.

Adafruit’s Ultimate GPS FeatherWing is shown
Adafruit Ultimate GPS FeatherWing

Using Adafruit’s GPS library, we can get our location like so:

import adafruit_gps
import board
import busio
import time

# Setup UART and the GPS module
uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10)
gps = adafruit_gps.GPS(uart, debug=False)

# Set our mode to GGA/RMC
gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")
# Request updates every second (sent in ms)
gps.send_command(b"PMTK220,1000")

last_print = time.monotonic()
while True:
current = time.monotonic()

# Parse any GPS data if available
gps.update()

# Every second, check if we have a location fix
# and print out our location if so
if current - last_print >= 1.0:
last_print = current
if not gps.has_fix:
print("Waiting for GPS location fix...")
continue

print(f"GPS Location: {gps.latitude}, {gps.longitude}")

Implementation

To check if we should open the garage door, the car’s Feather will use the GPS module to get its active position every second. It then uses the Haversine formula to calculate the distance from its location to the trigger zone. If it is within 15 meters, it’ll send out an ‘open door’ message. To prevent firing this message as we leave the neighborhood, we first have to be at least 200 meters away to ‘activate’ the trigger zone.

Animated data log from one of my initial tests (not to scale)

Sending Commands

All of my messages contained the following fields:

Message attributes are shown with descriptions. ID, Channel, Message, Target
Message attributes

Whenever the message is sent, we use Python’s struct library to pack the data into binary data. This keeps the payload size minimal while still giving us an easy way to decode the message later:

import struct

# Set the message format as four unsigned shorts
msg_format = '4H'

# Pack our example variables according to the format
packed = struct.pack(msg_format, 202, 20, 4, 429)

print(packed)
# b'\xca\x00\x14\x00\x04\x00\xad\x01'

# Unpack the data
msg_id, channel, message, target = struct.unpack(msg_format, packed)

print(msg_id, channel, message, target)
# 202 20 4 429

We have four total commands for this project:

A table is shown displaying the four command set up. Acknowledgement, Ping, Pong and Trigger.
Message commands

Whenever a message is received, an acknowledgment message is sent to confirm receipt (except for ping since we send a pong reply). This is because sometimes packets can be lost, even if the devices are within range. If an acknowledgment is not received back within five seconds, it’ll automatically resend the message until one is received. This helped solve some of the message delivery problems I was facing as I would lose a packet 5–10% of the time.

Conclusion

Overall, this was an incredibly fun project to set up and test out. I’ve been itching to get my hands dirty with a LoRa project for a few months and finally had a chance. I was thoroughly impressed by the range (even using such a simple antenna), with a more advanced setup the possibilities are endless I can only imagine.

For my next LoRa project, I may consider using a LoRa breakout versus an integrated solution like the Feather boards I used for this project. The limited resources were a major time sink during the development and limited how much I could do as I had to cut out quite a few planned features. I think if I had used C++ it would’ve been much easier so one of my major plans for next year is to skill up more with C++.

Thank you for reading, I hope this was an interesting read, and feel free to leave a comment with any questions. You can find the code repository below which contains all of the relevant code and some extra documentation.

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

--

--

Software Engineer with a passion for helping spread technical knowledge about my favorite hobbies from microcontrollers to automotive and much more.