Using CoAP on SARA N2 NB-IoT modules

UDP packets are generally nice but there’s one thing they are: Datagrams. You can’t tell for sure that they have been delivered or when. Usually this is solved by saying “use TCP if you want flow control” but when you are working on NB-IoT modules that might not be an option.

The problem

If you are sending UDP packets with the SARA N2 module you generally don’t know what will happen with the data once it is sent. It might linger in the IP stack for minutes or hours before it is sent and you don’t know if it has been received by the backend system. Ditto when sending data out to the devices – the packets are sent, but you don’t know if or when the device have received the packet. The network will buffer outgoing packets but if your device restarts and creates a new session the old packets will be dropped unceremoniously and you won’t know. As long as your device is powered up and running it will eventually receive the packets but you won’t know when it will receive them.

You can implement acknowledgements and retransmits on top of UDP but pretty soon you’ll see that it’s a lot of added complexity that you don’t want to handle.

Fortunately this is a problem that is solved. Say hi to CoAP, the Constrained Application Protocol defined in RFC 7252. – It solves all (or, to be perfectly honest, most) of your problems!

The protocol itself is loosely modeled on the way HTTP works and a reasonable implementation can be done in as little as a few hundred lines of code. Most embedded frameworks have an implementation that you can import straight away, such as Zephyr, mBed and Arduino.

Setting up CoAP in SARA N2 modules

The SARA N2 modules supports a limited subset CoAP out of the box but the way it is used might not be entirely intuitive. Before you begin you should register your device in the Horde console and set up the mda.ee APN.

Here’s how you set up the CoAP profile:

1. Create a CoAP profile

Start by defining a CoAP profile. This uses the AT+UCOAP commands. Horde listens on coap://172.16.15.14:5683/ and will respond to POST requests. The path will be included in the message metadata.

Each field is set up individually. AT+UCOAP=0 sets the IP address, AT+UCOAP=1 sets the path to use, AT+UCOAP=2 sets the option mask (ie what to include in the header of the request), AT+UCOAP=3 sets the profile number to use. The profile must be set to valid (AT+UCOAP=4,"1") before it is saved with AT+UCOAP=6,"<profile>".

This will create a new profile that points to coap://172.16.15.14:5683/request/uri and save it as profile 0:

AT+UCOAP=0,"172.16.15.14","5683"
AT+UCOAP=1,"/request/uri"
AT+UCOAP=2,"2","1"
AT+UCOAP=3,"0"
AT+UCOAP=4,"1"
AT+UCOAP=6,"0"

2. Load the profile

Once the profile is save to NVRAM it can be pulled back with AT+UCOAP=5,"<profile number>". You must select the AT command interface via the AT+USELCP command as well:

AT+UCOAP=5,"0"
AT+USELCP=1

Sending messages

Now that everything is set up you can send a message using the following command (4 sends as a POST and 0 sets the content type to plain text):

AT+UCOAPC=4,"48656C6C6F207468657265",0

Receiving messages

Horde currently supports three different ways of sending messages to devices:

1) Regular UDP. It’s simple, efficient and built into all available NB-IoT modules but it has its downsides. The biggest drawback is that UDP packets might get lost. If your device is in power save mode the network will buffer UDP packets to your device but you can’t restart the module since that will create a new session with (potentially) a new IP and the network will uncermoniously drop the packets. The second drawback is that you won’t get receive confirmation when you are sending something to the device from the outside (this is called “downstream” in radio parlance). The device might read the packet right away or wake up in a few hours and days and read the packet. Similarly packets going from the device to the outside (“upstream” in radio parlance) may be lost or dropped and the device can’t tell if someone is listening on the other side.

2) CoAP pull. Messages are buffered in Horde itself and will be sent to the device when it sends a CoAP GET request to Horde. Multiple data packets can be queued and will be sent FIFO style out to the device. It won’t matter if the device reboots between each request. Packets sent from the device are acknowledged by Horde when they are received (via a regular CoAP POST request).

3) CoAP push. This requires a CoAP server on the device and Horde will deliver messages via CoAP POST requests to the device. This ensures us that messages are confirmed by both the device and by Horde when they are sent.

Unfortunately SARA N2 modules doesn’t support running a CoAP server on the device so we won’t get message confirmations on downstream messages but we can use the GET requests from the CoAP client on the module to retrieve messages.

Using the Horde API to send messages

Let’s send a message via Horde to the device. The request is relatively straightforward; POST the following JSON structure /collections/{collectionID}/devices/{deviceID}/to:

{
  "transport": "coap-pull",
  "payload": "SGVsbG8gd29ybGQK"
}

The payload is base64 encoded. Use the base64 command line utility to encode a string, e.g.echo Hello world | base64.

The curl command to do this looks like this:

curl -XPOST \
    -HX-API-Token:<token> \
    -d'{"transport":"coap-pull", "payload":"SGVsbG8gd29ybGQK"} \
    https://api.nbiot.engineering/collections/{collectionID}/devices/{deviceID}/to

If you switch to your module and issue the command AT+UCOAPC=1 you should get an URC with the response from Horde after a second or two. The output is hex encoded:

AT+UCOAPC=1

OK

+UCOAPCD: "48656C6C6F20776F726C640A"

Upstream messages

Messages received by Horde will have the CoAP information attached as metadata in addition to a field named transport which shows which transport was used to send the message. For the message above the data message in /collections/{collectionId}/devices/{deviceId}/data and the outputs would look like this:

{
  "type": "data",
  "device": {
    "deviceId": "<device ID>",
    "collectionId": "<collection ID>",
    "imei": "<IMEI>",
    "imsi": "<IMSI>",
  },
  "payload": "SGVsbG8gdGhlcmU=",
  "received": 1553867004477,
  "transport": "coap-push",
  "coapMetaData": {
    "code": "POST",
    "path": "request/uri"
  }
}

Depending on what transport the message arrived on the metadata field might be coapMetaData or udpMetaData. The path parameter in the CoAP POST request is added to the metadata, making it possible to filter on the messages in your backend system.