With the recent release of Notecard LoRa, we at Blues now offer Notecards that provide wireless connectivity for cellular, Wi-Fi, and LoRa. Even cooler—all Notecards share the same API structure and firmware libraries, meaning you can write firmware that works the same regardless of whether you’re communicating over cellular, WiFi, or LoRa.
Writing connectivity-agnostic firmware can give you a lot of flexibility for deployments. For example, you could use LoRa for devices in areas with LoRaWAN coverage, Wi-Fi for devices in areas with consistent Wi-Fi coverage, and cellular for devices in rural areas or devices that move around.
In this article we’ll build an example firmware program that runs on all three connectivity methods. The example will take temperature readings at a regular interval, send those readings to the cloud regularly, and provide environment variables for the user to manage their devices in the field. Along the way we’ll look at tips & tricks for writing firmware that works with all types of Notecards.
Let’s get started.
The full source code for this article’s example is available here.
Getting Set Up
Before getting started two notes about the tools I’ll be using.
-
My example uses the Notecard’s Arduino Firmware Library and runs on the Blues Swan. You can replicate my code with any of the Notecard’s other firmware libraries, and run your code on virtually any other host. If you’re having trouble porting this article’s code to other boards and languages feel free to reach out in our community forum.
-
If this is your first time seeing firmware that communicates with the Notecard, I’d recommend completing our Collecting Sensor Data tutorial before continuing, as it explains the basics of how the Notecard works, how to communicate with the Notecard from a host, and how to load your Arduino firmware onto your host.
Ok, onto the firmware. Let’s start with the overall structure, as the boilerplate below shows how I set up most of my Notecard firmware programs.
#include <Arduino.h>
#include <Notecard.h>
#define usbSerial Serial
// See https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid
#define PRODUCT_UID "<your-product-uid>"
Notecard notecard;
void setup()
{
usbSerial.begin(115200);
notecard.setDebugOutputStream(usbSerial);
notecard.begin();
// code for setup
}
void loop()
{
// code that should run in a loop
}
This is a pretty standard Arduino skeleton, where the setup()
function runs once and the loop()
function runs infinitely. To start our firmware discussion let’s first look at the code that belongs within setup()
.
card.version
The card.version
request provides metadata about the Notecard, including its firmware version. The card.version
request also provides a Notecard’s SKU, which you might find useful if you need to write conditional code that checks what type of Notecard you’re working work.
The code below prints the SKU to the serial log, but you may wish to save the SKU to a variable if your firmware needs to use it in other places.
if (J *req = notecard.newRequest("card.version"))
{
J* rsp = notecard.requestAndResponse(req);
usbSerial.print("Notecard SKU: ");
usbSerial.println(JGetString(rsp, "sku"));
notecard.deleteResponse(rsp);
}
card.wifi
Notecards with Wi-Fi connectivity need to know the SSID and password of the Wi-Fi network they should use. For most scenarios we recommend setting your Notecard’s Wi-Fi information using the device’s onboard AP button, as it allows you to keep all Wi-Fi-specific code out of your firmware.
However, during development you may want to use the Notecard’s card.wifi
request for easy testing. This technique is also useful if you need to dynamically switch a Notecard’s Wi-Fi network.
#define WIFI_SSID "<your-ssid>"
#define WIFI_PASSWORD "<your-password>"
...
if (J *req = notecard.newRequest("card.wifi"))
{
JAddStringToObject(req, "ssid", WIFI_SSID);
JAddStringToObject(req, "password", WIFI_PASSWORD);
if (!notecard.sendRequest(req))
{
usbSerial.println("Notecard does not support Wi-Fi connectivity.");
}
}
The cool thing about this code is, even though it is Wi-Fi-specific, there’s no harm in running it on all Notecards. Notecards that don’t support Wi-Fi will log the "Notecard does not support Wi-Fi connectivity."
message and move on.
hub.set
The hub.set
request tells the Notecard how to communicate with its backing cloud service, Notehub. The request takes a product
(a reference to a specific Notehub project) as well as a mode
, which defines the Notecard’s synchronization mode.
The code below tells the Notecard to connect to Notehub periodically ("mode": "periodic"
), to send data out every hour "outbound": 60
, and to check for incoming data or environment variables every four hours "inbound: 240"
.
if (J *req = notecard.newRequest("hub.set"))
{
JAddStringToObject(req, "product", PRODUCT_UID);
JAddStringToObject(req, "mode", "periodic");
JAddNumberToObject(req, "outbound", 60);
JAddNumberToObject(req, "inbound", 60 * 4);
notecard.sendRequest(req);
}
There are a number of different ways you may want to configure your hub.set
request, but I find these settings to be a reasonable default for projects running on battery power.
Regardless of how you configure your hub.set
, one cool thing to notice is that none of this configuration is specific to cellular, Wi-Fi, or LoRa; the Notecard takes care of the messy connection details regardless of the radio access technology you’re using.
note.template
When you work with data on the Notecard you work with Notes. Notes are JSON objects that the Notecard uses to store data and as a basis for transmitting data to and from Notehub.
On bandwidth-limited technologies like LoRa and NTN (Non-Terrestrial Networks, which Starnote uses), the Notecard requires all Notes to use templates. Templates tell the Notecard the format of the data you intend to use, and allow the Notecard to store and transmit your data using as little memory and bandwidth as possible.
You can create a Note template using the note.template
request, and the example below shows how to use the request to create a template for a new sensors.qo
Notefile.
if (J *req = notecard.newRequest("note.template"))
{
JAddStringToObject(req, "file", "sensors.qo");
JAddNumberToObject(req, "port", 1);
JAddStringToObject(req, "format", "compact");
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "temp", 14.1);
JAddNumberToObject(body, "_time", 14);
notecard.sendRequest(req);
}
You can refer to the note.template
request’s API reference for full details on what each of these fields mean, but the most important part is the contents of the body
. In this case the body
has two entries:
-
"temp": 14.1
: This tells the Notecard to store and transmit temperature readings as 4-byte floating point numbers. -
"_time": 14
: This is a special identifier that tells the Notecard to generate a timestamp when Notes are created for this Notefile, and to transmit that timestamp to Notehub as a 4-byte integer.
In general, when defining a template you define all of the data fields you’d like to work with in your body
, and then use this list of Notecard data types to figure out the best way to represent each field in a template.
env.template
Environment variables are a state-and-settings-management feature you can use to share data between devices, or even fleets of devices. For this example we’re going to use a single environment variable to store how often our device should take temperature readings, in minutes.
Much like the Notecard requires Note templates when working with Notes on bandwidth-constrained protocols, the Notecard requires you to define environment variable templates when using environment variables on bandwidth-constrained protocols.
You can create the required template using the env.template
request, which has an API that is very similar to the note.template
request. The code below uses env.template
to tell the Notecard about a single environment variable, reading_interval
, and sets its value to 12
, which represents a 2-byte signed integer (e.g. -32,768 to 32,767).
if (J *req = notecard.newRequest("env.template"))
{
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "reading_interval", 12);
notecard.sendRequest(req);
}
Now that the Notecard knows about the reading_interval
environment variable you’re free to define that variable on your device, fleet, or project. In the next section we’ll look at how to retrieve the value for reading_interval
, and how to provide a fallback in case that variable doesn’t exist on a device.
And with this request done, we’re ready to move on from setup and look at the main loop of our program.
Loop
As a reminder of where we’re at, let’s return to the boilerplate of our firmware I introduced at the beginning of this article.
#include <Arduino.h>
#include <Notecard.h>
#define usbSerial Serial
// See https://dev.blues.io/notehub/notehub-walkthrough/#finding-a-productuid
#define PRODUCT_UID "<your-product-uid>"
Notecard notecard;
void setup()
{
usbSerial.begin(115200);
notecard.setDebugOutputStream(usbSerial);
notecard.begin();
// code for setup
}
void loop()
{
// code that should run in a loop
}
In the previous section we looked at the code that belongs in the setup()
function that logged Notecard metadata, optionally configured a Wi-Fi connection, and set up templates for working with Notes and environment variables.
In this section we’ll look at the contents of the loop()
function, which for this example will take a temperature reading, place that temperature reading into a Note, and use the reading_interval
environment variable to schedule future sensor readings.
Let’s start by looking at how to take a temperature reading from the Notecard.
card.temp
The Notecard comes with an onboard temperature sensor you can use with the card.temp
request. The code below shows how to perform this request for our example.
float getTemperature()
{
float temp = 0;
J *req = notecard.newRequest("card.temp");
if (J *rsp = notecard.requestAndResponse(req))
{
temp = JGetNumber(rsp, "value");
notecard.deleteResponse(rsp);
}
return temp;
}
void loop()
{
float temperature = getTemperature();
usbSerial.print("Temperature = ");
usbSerial.print(temperature);
usbSerial.println(" *C");
...
}
note.add
With temperatures readings coming in, let’s next place the temperature data in a Note and queue it on the Notecard. To do this you can use the Notecard’s note.add
request as shown below, placing your data in the body
attribute.
float temperature = getTemperature();
if (J *req = notecard.newRequest("note.add"))
{
JAddStringToObject(req, "file", "sensors.qo");
J *body = JAddObjectToObject(req, "body");
JAddNumberToObject(body, "temp", temperature);
notecard.sendRequest(req);
}
With this code running your firmware is now continuously taking temperature readings, and adding those readings into Notes that are queued on the Notecard.
The Notecard transmits this queue to Notehub according the settings you provide in your hub.set
request. For this example we set our Notecard to {"mode":"periodic","outbound":60}
, which tells the Notecard to send all outbound data to Notehub every 60 minutes. During testing you may wish to change your outbound
to a much lower value to make debugging easier.
With the temperature readings now being placed in Notes, let’s last look at how to control the interval between readings.
env.get
The current version of our firmware takes sensor readings and queues them in a loop repeatedly. To keep the firmware from overflowing memory we need to add a delay between readings, and the most common way to do that in Arduino is with the delay
function. For example, the code below adds one-minute delay to each execution of loop()
.
void loop()
{
...
delay(1000);
}
This approach works, but if you’ll recall from an earlier section we want to use the reading_interval
environment variable to control how often to wait in between loops.
To get the value of an environment variable on the Notecard you can use the env.get
request, which takes the name
of the variable you’d like to retrieve. The code below gets the value of the reading_interval
variable (falling back to 30 minutes if the variable is not defined), and calls the Arduino delay()
function with the customizable value.
int getSensorInterval()
{
int sensorIntervalMinutes = 30;
if (J *req = notecard.newRequest("env.get"))
{
JAddStringToObject(req, "name", "reading_interval");
J* rsp = notecard.requestAndResponse(req);
int readingIntervalEnvVar = atoi(JGetString(rsp, "text"));
if (readingIntervalEnvVar > 0) {
sensorIntervalMinutes = readingIntervalEnvVar;
}
notecard.deleteResponse(rsp);
}
return sensorIntervalMinutes;
}
void loop()
{
...
int sensorIntervalMinutes = getSensorInterval();
usbSerial.print("Delaying ");
usbSerial.print(sensorIntervalMinutes);
usbSerial.println(" minutes");
delay(sensorIntervalMinutes * 60 * 1000);
}
The big advantage of this approach is the flexibility it offers. Instead of hardcoding a delay into your firmware, you can instead customize your delay in Notehub or through the Notehub API.
One note when testing: by default the Notecard checks for environment variable updates when it performs an inbound synchronization. For our example we configured the Notecard to perform an inbound synchronization every four hours {"req":"hub.set","inbound":60*4}
. You may wish to set this value to a far smaller number during testing, or temporarily use {"req":"hub.set","mode":"continuous","sync":true}
, which uses more voltage, but tells to the Notecard to maintain a constant connection to Notehub, and to also immediately synchronize environment variable changes.
When you have your settings where you want them, our firmware example is now complete! If you run the firmware on your device you should see a steady stream of readings and debugging information in the serial log. The screenshot below shows the output from firmware running on my device with inbound
, outbound
, and reading_interval
all set to 1
for easy testing.
And on Notehub you should see a steady stream of temperature readings in your project’s events.
The full source code for this article’s example is available here.
Wrapping Up
In this article we built a firmware program that takes temperature readings at a regular interval and sends them to the cloud using the Notecard.
And although we covered a number of different topics, notice how—with the exception of the optional section on providing Wi-Fi credentials—none of this article’s steps were specific to an individual radio access technologies.
This is the real power of the Notecard. Instead of worrying about the messy details inherent in working with network communication, you can focus your efforts on solving your business or customer problem, and let the Notecard to handle your connectivity through a handful of simple JSON APIs.
If you’d like to give this a try, the easiest way to get started is with a Starter Kit from the Blues shop. And if you have have any questions about this article’s code feel free to reach out in the comments.