We'll want to start by loading the code onto the Feather sensor nodes. To do this, copy the code below and rename it as code.py.
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries # # SPDX-License-Identifier: MIT """ lora_device.py CircuitPython LoRa Device w/BME280 """ import time import busio import digitalio import board # Import LoRa Library import adafruit_rfm9x # Import BME280 Sensor Library import adafruit_bme280 # Device ID FEATHER_ID = 0x01 # Delay between sending radio data, in minutes. SENSOR_SEND_DELAY = 1 # Create library object using our Bus I2C port i2c = busio.I2C(board.SCL, board.SDA) bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c) # Define radio frequency, MUST match gateway frequency. RADIO_FREQ_MHZ = 905.5 # Define pins connected to the chip, use these if wiring up the breakout according to the guide: # pylint: disable=c-extension-no-member CS = digitalio.DigitalInOut(board.RFM9X_CS) # pylint: disable=c-extension-no-member RESET = digitalio.DigitalInOut(board.RFM9X_RST) # Define the onboard LED LED = digitalio.DigitalInOut(board.D13) LED.direction = digitalio.Direction.OUTPUT # Initialize SPI bus. spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) # Initialze RFM radio rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ) # Set transmit power to max rfm9x.tx_power = 23 # sensor data bme280_data = bytearray(8) while True: # Get sensor readings temp_val = int(bme280.temperature * 100) print("\nTemperature: %0.1f C" % bme280.temperature) humid_val = int(bme280.humidity * 100) print("Humidity: %0.1f %%" % bme280.humidity) pres_val = int(bme280.pressure * 100) print("Pressure: %0.1f hPa" % bme280.pressure) # Build packet with float data and headers # packet header with feather node ID bme280_data[0] = FEATHER_ID # Temperature data bme280_data[1] = (temp_val >> 8) & 0xff bme280_data[2] = temp_val & 0xff # Humid data bme280_data[3] = (humid_val >> 8) & 0xff bme280_data[4] = humid_val & 0xff # Pressure data bme280_data[5] = (pres_val >> 16) & 0xff bme280_data[6] = (pres_val >> 8) & 0xff bme280_data[7] = pres_val & 0xff # Convert bytearray to bytes bme280_data_bytes = bytes(bme280_data) # Send the packet data print('Sending data...') LED.value = True rfm9x.send(bme280_data_bytes) print('Sent data!') LED.value = False # Wait to send the packet again time.sleep(SENSOR_SEND_DELAY * 60)
We'll also want to set the frequency which we are transmitting on. This value must be the same between the Feathers (nodes) and the Raspberry Pi (gateway), otherwise the gateway will not receive the packet, or visa-versa.
To do this, set RADIO_FREQ_MHZ
to a frequency value in MHz. For example, I'll set it to 905.5MHz.
# Define radio frequency, must match device. RADIO_FREQ_MHZ = 905.5
We also need a way for the gateway to identify each Feather. To do this, we'll set the FEATHER_ID
. If you're preparing the first sensor node, leave this variable alone as its already set to 1.
# Device ID FEATHER_ID = 1
Save the file (CTRL/CMD+S). This will reload and run the code from code.py.
Using the Mu editor, there is a built in REPL/Serial monitor. This is the recommended way to view output from your Python code. Alternatively you can use a terminal program to connect to the Feather's USB serial port.
You should see the following output from the REPL:
code.py output: Temperature: 23.8 C Humidity: 21.5 % Pressure: 1005.2 hPa Sending data.. Data sent!
Whenever the Feather node(s) send data using the radio, they'll blink an onboard LED (located on Pin #13) to show that the packet has been sent.
One of the advantages of using LoRa over another type of transport (like WiFi) is that the radio is only turned on during transmission. Doing this reduces the amount of power consumption by the node.
We're going to build a packet to transmit the data to the gateway. This packet comprises of 7 bytes (the RFM9x library currently supports sending packets <= 252 bytes in length) which will hold all of our required sensor data, along with a unique identifier for our node, set in the code as FEATHER_ID
.
The temperature values read by the BME280 are floating point (decimal) values (this is a value like 3.14 instead of 3). To send these values, we'll assemble the packet by sending the values as integers (which will later be re-assembled by the gateway)
Once assembled, the LoRa packet is sent using the radio.
rfm9x.send(bme280_data)
To modify how frequently the packet is sent, change the variable SENSOR_SEND_DELAY
, keeping in mind that the delay is in minutes.
That's it! Then, load the code onto the other feather(s) sensor nodes, changing the FEATHER_ID
as you go.
Next, we'll want to set up the Raspberry Pi with the code to use the RFM9x as a LoRa gateway. Save the code below on your Pi as lora_gateway.py.
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries # # SPDX-License-Identifier: MIT """ Adafruit IO LoRa Gateway Learn Guide: https://learn.adafruit.com/multi-device-lora-temperature-network by Brent Rubell for Adafruit Industries """ # Import Python System Libraries import time # Import Adafruit IO REST client. from Adafruit_IO import Client # Import Blinka Libraries import busio import board from digitalio import DigitalInOut, Direction, Pull # Import SSD1306 module. import adafruit_ssd1306 # Import RFM9x module import adafruit_rfm9x # Define radio frequency, must match device. RADIO_FREQ_MHZ = 905.5 # Button A btnA = DigitalInOut(board.D5) btnA.direction = Direction.INPUT btnA.pull = Pull.UP # Button B btnB = DigitalInOut(board.D6) btnB.direction = Direction.INPUT btnB.pull = Pull.UP # Button C btnC = DigitalInOut(board.D12) btnC.direction = Direction.INPUT btnC.pull = Pull.UP # Create the I2C interface. i2c = busio.I2C(board.SCL, board.SDA) # 128x32 OLED Display display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c, addr=0x3c) # Clear the display. display.fill(0) display.show() width = display.width height = display.height # Configure LoRa Radio CS = DigitalInOut(board.CE1) RESET = DigitalInOut(board.D25) spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ) prev_packet = None # Set to your Adafruit IO username. # (go to https://accounts.adafruit.com to find your username) ADAFRUIT_IO_USERNAME = 'USER' # Set to your Adafruit IO key. ADAFRUIT_IO_KEY = 'KEY' # Create an instance of the REST client. aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) # Set up Adafruit IO feeds temperature_feed_1 = aio.feeds('feather-1-temp') humidity_feed_1 = aio.feeds('feather-1-humid') pressure_feed_1 = aio.feeds('feather-1-pressure') temperature_feed_2 = aio.feeds('feather-2-temp') humidity_feed_2 = aio.feeds('feather-2-humid') pressure_feed_2 = aio.feeds('feather-2-pressure') def pkt_int_to_float(pkt_val_1, pkt_val_2, pkt_val_3=None): """Convert packet data to float. """ if pkt_val_3 is None: float_val = pkt_val_1 << 8 | pkt_val_2 else: float_val = pkt_val_1 << 16 | pkt_val_2 << 8 | pkt_val_3 return float_val/100 while True: packet = None # draw a box to clear the image display.fill(0) display.text('Adafruit.IO LoRa GTWY', 0, 0, 1) # check for packet rx packet = rfm9x.receive() if packet is None or prev_packet: display.show() display.text('- Waiting for PKT -', 10, 20, 1) else: prev_packet = packet print('> New Packet!') # Decode packet temp_val = pkt_int_to_float(packet[1], packet[2]) humid_val = pkt_int_to_float(packet[3], packet[4]) pres_val = pkt_int_to_float(packet[5], packet[6], packet[7]) # Display packet information print('Device ID: LoRa Feather #', packet[0]) print("Temp: %0.2f C" % temp_val) print("Humid: %0.2f %% " % humid_val) print("Pressure: %0.2f hPa" % pres_val) # Send to Feather 1 feeds if packet[0] == 0x01: display.fill(0) display.text('Feather #1 Data RX''d!', 15, 0, 1) display.text('Sending to IO...', 0, 20, 1) display.show() aio.send(temperature_feed_1.key, temp_val) aio.send(humidity_feed_1.key, humid_val) aio.send(pressure_feed_1.key, pres_val) display.text('Sent!', 100, 20, 1) display.show() # Send to Feather 2 feeds if packet[0] == 0x02: display.fill(0) display.text('Feather #2 Data RX''d!', 15, 0, 1) display.text('Sending to IO...', 0, 20, 1) display.show() aio.send(temperature_feed_2.key, temp_val) aio.send(humidity_feed_2.key, humid_val) aio.send(pressure_feed_2.key, pres_val) display.text('Sent!', 100, 20, 1) display.show() time.sleep(1) display.show()
We'll need to install the Adafruit IO Python library to forward the messages by the LoRa radio to Adafruit IO. To do this, enter the following in your terminal:
sudo pip3 install adafruit-io
Once the Adafruit IO library is installed, we'll need to set the username and the key in the code:
Set the ADAFRUIT_IO_USERNAME
to your Adafruit IO Username.
Then, set the ADAFRUIT_IO_KEY
to your Adafruit IO Key.
And save the file.
Now that the Adafruit IO username and password are configured, run the code by typing the following into your terminal:
python3 lora_gateway.py
The display should show Adafruit IO GTWY and - Waiting for PKT -. This is the idle state, the gateway is listening for a new packet from the Feather node.
When a new packet is received, the display will change to show that a new packet from the feather was received. It'll also forward the packet to Adafruit IO.
The terminal will change when a new packet is received:
- New Packet! - Device ID: LoRa Feather # 1 Temp: 22.83 C Humid: 17.90 % Pressure: 370.11 hPa - New Packet! - Device ID: LoRa Feather # 2 Temp: 22.89 C Humid: 10.48 % Pressure: 370.11 hPa
To verify Adafruit IO has received the data, navigate to the monitor page on Adafruit IO. This page shows a live-view of all incoming data (and error messages).
Your Adafruit IO Dashboard should look like the following as it receives data from the LoRa devices:
If you want this code to run when the Pi boots up, you'll need to make an entry in rc.local.
To do this, type the following into the terminal
sudo nano /etc/rc.local
Then, add the following line of code the file
(sleep 30; python3 home/pi/rfm9x_gateway.py) &
and save the file.
Reboot the Pi by entering the following into the terminal
sudo shutdown -r now
When the Pi restarts, it'll run the gateway code.
Congrats! You've built your first LoRa network, and forwarded data to the internet.
Since the network topology can seem complicated at a glance, we'll take it piece by piece and explain how the code relates.
First, the gateway receives a new packet.
packet = rfm9x.receive()
Then, it checks if the packet's value is empty or a retransmission. If it's a new packet value, it'll decode the packet.
Since we converted the float sensor value into an integer for transmission, and know the order of the packet, we can decode the temperature, humidity and air pressure. We also know the first byte of the packet (the header) is the the Feather's unique identifier.
# Decode packet feather_id = packet[0] temp_val = pkt_int_to_float(packet[1], packet[2]) humid_val = pkt_int_to_float(packet[3], packet[4]) pres_val = pkt_int_to_float(packet[5], packet[6])
Now that the data has been decoded, we're going to tackle the second role of the Radio Bonnet - sending the data as a transmission to Adafruit IO.
If the decoded feather_id
is 1, we'll send the values to Adafruit IO feeds temperature_feed_1
, humidity_feed_1
, and pressure_feed_1
.
if feather_id is 1: display.fill(0) display.text('Feather #1 Data RX''d!', 15, 0, 1) display.text('Sending to IO...', 0, 20, 1) display.show() aio.send(temperature_feed_1.key, temp_val) aio.send(humidity_feed_1.key, humid_val) aio.send(pressure_feed_1.key, pres_val) display.text('Sent!', 100, 20, 1) display.show()
We have code for detecting a second feather, but if you have a third (or fourth!), you'll want to add another elif
statement identical to the second one.
Text editor powered by tinymce.