empowering creative people

Send a Downlink from Adafruit.io via The Things Network

Any good LoRaWAN device can send data up to the cloud, but now its time to send data back down to the device. TTN Fair Access Policy limits the data each device can send. A TTN device can use 30 seconds of uplink time per day, per device. At most, you can send 10 downlink messages per day. This is including the ACKs for confirmed uplinks. Obviously, we need to use our uplinks and downlinks wisely and create devices that are focused on uplinking data rather than downlinking. That said, downlinks can be invaluable. From sending confirmation that a payload was received properly, to changing a configuration or even just acting as a switch, downlinks are very useful. Sure, we could show you how to send a downlink to your Pycom Lopy4 by using the downlink field in TTN, but where is the magic in that? TTN isn't the user interface you want to use for your project, we want buttons! So let's take it one step farther and show you how you can make a button in Adafruit.io that will send a simple downlink to your device!

For this tutorial we will use The Things Network, IFTTT and Adafruit.io, if you don't have accounts with these services then you should open them now. We will need to have each open in a tab and jump between them. We will be using the same code for our Pycom Lopy4 as used in our Getting Started on The Things Network Tutorial. If you aren't connected to TTN yet then head back to that tutorial and get connected first! 

The Code

Here is the code that we will be using for this example:

from network import LoRa
import socket
import utime
import binascii
import pycom
import ustruct
import machine
from machine import ADC

# takes battery voltage readings
def adc_battery():
	adc = ADC(0)                                        # initialise adc hardware    
	adc_c = adc.channel(attn=3, pin='P16')              # create an object to sample ADC on pin 16 with attenuation of 11db (config 3)    
	adc_samples = []                                    # initialise the list    
	for count in range(100):                            # take 100 samples and append them into the list

	adc_samples = sorted(adc_samples)                   # sort the list    
	adc_median = adc_samples[int(len(adc_samples)/2)]   # take the center list row value (median average)
	# apply the function to scale to volts
	adc_median = adc_median * 2 / 4095 / 0.3275

	return adc_median

# disable LED heartbeat (so we can control the LED)
# set LED to red

# lora config
lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.AS923)
# access info
app_eui = binascii.unhexlify('XXXXXXXXXXXXX')

# attempt join - continues attempts background
lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0)

# wait for a connection
print('Waiting for LoRaWAN network connection...')
while not lora.has_joined():
	# if no connection in a few seconds, then reboot
	if utime.time() > 15:
		print("possible timeout")

# we're online, set LED to green and notify via print
print('Network joined!')

# setup the socket
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
s.setsockopt(socket.SOL_LORA, socket.SO_DR, 5)

# sending some bytes
print('Sending 1,2,3')
s.send(bytes([1, 2, 3]))

#text is automatically converted to a string, data heavy (dont do it this way)
print('Sending "Hello World"')
s.send("Hello World!")

count = 0
# limit to 200 packets; just in case power is left on
while count <= 200: 
	lipo_voltage = adc_battery()

	print("Battery voltage:  ", lipo_voltage)
	# encode the packet, so that it's in BYTES (TTN friendly)
	# could be extended like this struct.pack('f',lipo_voltage) + struct.pack('c',"example text")
	packet = ustruct.pack('f',lipo_voltage)

	# send the prepared packet via LoRa

	# example of unpacking a payload - unpack returns a sequence of
	#immutable objects (a list) and in this case the first object is the only object
	print ("Unpacked value is:", ustruct.unpack('f',packet)[0])

	# check for a downlink payload, up to 64 bytes
	rx_pkt = s.recv(64)

	# check if a downlink was received
	if len(rx_pkt) > 0:
		print("Downlink data on port 200:", rx_pkt)
		input("Downlink recieved, press Enter to continue")

	count += 1

The important part of the code is the received section. Inside the while loop, we listen for received packets and store them as a variable. We can then include logic of any kind we like to analyze the incoming data. In this case, we just make sure it's more than zero bytes.

    # check for a downlink payload, up to 64 bytes
    rx_pkt = s.recv(64)

    if len(rx_pkt) > 0:
        print("Downlink data on port 200:", rx_pkt)
        input("Downlink recieved, press Enter to continue")

Every time that we receive a downlink payload we will get the message in REPL that a message has been received. The LED will turn orange, and the program will wait until we strike ENTER to continue.


Log in to your Adafruit.io account, and create a new feed called ButtonPress.


Add a momentary push button to your dashboard. We've made mine display the text "Downlink". When it's pressed it writes the number "1" to the feed "ButtonPress". When the button is released it writes "0". This turns the button press into an on and off value stored in the feed we just created. All the other settings we left as the default.


Go ahead and test your button and make sure you are getting values saved to your ButtonPressed feed.


We will use the online service at RequestBin to view the JSON data sent from our Pycom Lopy4 to the cloud. This data can be used to interface with just about any MQTT server.


Open RequestBin in a new tab, and Create a RequestBin. Copy the address of your bin to the clipboard. This will allow us to view the JSON data that is sent from TTN. RequestBin creates a temporary bin to catch the API data! We will come back to this later on.


The Things Network

Much like forwarding data from The Things Network, we will use integrations to send a downlink. Navigate to the Application Overview screen, and select Integrations. Choose HTTP integration.


In the HTTP Integration settings, we will set the access key to "Default Key". Paste the bin URL copied from RequestBin into the URL section. Set the Method to POST, and leave the rest of the fields empty.


Back in RequestBin, we will refresh the page to initiate monitoring. The Things Network will be passing its payload data up to RequestBin where you can view it in its raw JSON format. Either turn on your device so packets are being uplinked, or send a simulate an uplink from within TTN to see the data come across. RequestBin will not save this data for later use. Here we can see a received payload:


In the Raw Body in green, we can see all the data that is forwarded along with our humble payload. There is a lot there! The only thing we really want at this step is the Downlink URL. This is the last field of the raw body, properly formatted JSON data sent to this address will be downlinked to our device! If you want to know what else is there in the uplink data, it's all explained in The Things Network HTTP Integration Documentation. The downlink URL follows the following format:


You can skip the RequestBin process and just work out your downlink URL, but we found this method to be easier. A single mistake in the URL would derail the whole process!


We will be using IFTTT to pass our data from Adafruit.io to TTN. We are using IFTTT because the data sent to TTN needs to have a specific JSON format, and we can't edit our JSON messages sent from the free Adafruit.io. We will send a webhook from Adafruit.io to IFTTT, change the format and forward it to TTN. 

In IFTTT, create a new applet, choose Webhooks "Receive a web request" as the trigger. We'll call the event "ButtonPress".

In the URL field, we will paste the downlink URL copied from RequestBin.

For Method choose POST or PUT. The content type will be Application/json, and in the body we will send a message that follows one of these formats.

For RAW JSON the payload is already in bytes and this value is passed straight to your device:

"dev_id": "my-dev-id", // The device ID
"port": 1, // LoRaWAN FPort
"confirmed": false, // Whether the downlink should be confirmed by the device
"payload_raw": "AQIDBA==" // Base64 encoded payload: [0x01, 0x02, 0x03, 0x04]

We can also send data with payload fields. For this to work you will need to have an encoder script in place to encode the values to bytes.

"dev_id": "my-dev-id", // The device ID
"port": 1, // LoRaWAN FPort
"confirmed": false, // Whether the downlink should be confirmed by the device
"payload_fields": { // The JSON object to be encoded by your encoder payload function
"on": true,
"color": "blue"

In this example, we use the Raw Format and change "my-dev-id" to the Device ID that we want to downlink to.

Save the applet and then navigate to the webhooks page:


In the Webhooks Documentation, we will find the address to trigger our applet. Enter ButtonPress into the address, and then copy it to the clipboard.


You can test everything up to this point by clicking TEST on the Documentation page. This will trigger your webhook and your device should receive a downlink. We aren't done yet! Now let's get our button working on Adafruit.io


Create a new trigger in Adafruit.io. When prompted choose "Reactive Trigger". The trigger fields should be completed to match "If ButtonPress is equal to Comparison Value or Feed: then Send a Webhook Message to: https://maker.ifttt.com/trigger/buttonpress/with/key/"Your-Key-Here" 

You can now go back to the Dashboard and test the button!


We did it! The button Triggered an event, and we sent a webhook request to IFTTT. That webhook request activated our applet and sent our message to TTN for the downlink!


Using this method button presses can be passed almost instantly to our device. It’s worth noting that Adafruit.io limits basic users to one trigger every ten minutes. If you need to be able to trigger more frequently you can just create two buttons and two trigger commands that send a webhook to the same applet. Keep in mind that you are limited to 10 downlinks a day by TTN, so use them wisely! 
While TTN is still Class B, you cannot just send a downlink whenever you want. When a downlink is sent to TTN they get "scheduled". TTN holds the downlink until an uplink message comes through from the device, then the downlink is sent. So if your device never uplinks it will never receive a downlink.

Soon TTN will be Class C, and we will be able to send downlinks instantly (isn't the future exciting!). 

Going Further

This tutorial demonstrates a basic framework for sending a downlink using HTTP integration. It's not the most elegant solution, but it's one that should be accessible to the average maker. If you want to learn more about TTN and LoRaWAN, check out our The Things Network Tutorials. In these examples, we use the Pycom Lopy4 and Pioneers Platform. For more about Pycom devices, head over to our Pycom Tutorials!

Any good LoRaWAN device can send data up to the cloud, but now its time to send data back down to the device. TTN Fair Access...

Have a question? Ask the Author of this guide today!