Remote Command and Control
The Notecard's communication with its cloud backend, Notehub, is bidirectional. This means that in addition to sending local data from the Notecard to the cloud, you can also send data or commands in the opposite direction (cloud to device) using a pattern we call command and control.
Because the Notecard is a generic data pump, the commands you send to your devices can be as simple or as complex as you need them to be. For example you could tell a light to turn off, a robot to move left, or a machine to stop operating.
The Notecard allows you to send these commands remotely, giving you the ability to perform actions on devices located anywhere in cellular range. Let's look at how it works.
Sending Commands to Devices
The easiest way to send a command to a device is with the Notehub API's
note.add
request.
If you're new to the Notehub API, you'll want to read our Notehub API tutorial before continuing, as the tutorial shows you the basics of using the API, including how to generate a bearer token.
To use the note.add
request you need to send a POST
to the Notehub API's
/projects/<projectUID>/devices/<deviceUID>/notes/<file>
endpoint, where
projectUID
is your ProjectUID, deviceUID
is the DeviceUID of a device within
your Notehub project, and <file>
is the name Notefile you'd like to use (e.g.
data.qi
).
The body
of the note.add
request can be any JSON object you'd like. For
example, the curl
command below shows how to send {"command": "on"}
to a
device.
curl -X POST
-L 'https://api.notefile.net/v1/projects/<projectUID>/devices/<deviceUID>/notes/<file>'
-H 'Authorization: Bearer <access_token>'
-d '{"body": {"command": "on"}}'
Receiving Commands on a Device
Once you have Notes queued in Notehub, you next need to receive those Notes on
your device. You can configure how often your device checks for inbound notes
using the hub.set
request's
mode
, inbound
, and sync
arguments.
For example, running the command below places a Notecard in continuous mode.
Devices in continuous mode maintain a constant connection with Notehub, but
also use considerably more battery to maintain the connection. Setting the
request's sync
argument to true
ensures inbound notes sync as soon as
they're detected on Notehub.
{
"req": "hub.set",
"mode": "continuous",
"sync": true
}
J *req = NoteNewRequest("hub.set");
JAddStringToObject(req, "mode", "continuous");
JAddBoolToObject(req, "sync", true);
NoteRequest(req);
req = {"req": "hub.set"}
req["mode"] = "continuous"
req["sync"] = True
card.Transaction(req)
Check out our minimizing latency guide for additional information on sending low-latency commands.
If your project is more battery-conscious, you may wish to instead place your
device in periodic mode. Devices in periodic mode check for inbound Notes at a
given interval, which you can specify with the inbound
argument (in minutes).
The request below tells the Notecard to check for inbound Notes every 60 minutes.
{
"req": "hub.set",
"mode": "periodic",
"inbound": 60
}
J *req = NoteNewRequest("hub.set");
JAddStringToObject(req, "mode", "periodic");
JAddNumberToObject(req, "inbound", 60);
NoteRequest(req);
req = {"req": "hub.set"}
req["mode"] = "periodic"
req["inbound"] = 60
card.Transaction(req)
If you place your device in periodic mode, you can use the
hub.sync
request
while prototyping, as it triggers a synchronization of Notes between your
Notecard and Notehub, even if your device is in periodic mode.
Regardless of the configuration you use, once your device has received Notes
you can check if any are present by running a
note.changes
request.
The note.changes
request returns multiple Notes (if present), so you can
use the request to process multiple commands at once.
{
"req": "note.changes",
"file": "data.qi"
}
J *req = NoteNewRequest("note.changes");
JAddStringToObject(req, "file", "data.qi");
NoteRequest(req);
req = {"req": "note.changes"}
req["file"] = "data.qi"
rsp = card.Transaction(req)
Resulting in the following JSON response:
{
"notes": {
"1:8572": {
"body": {
"command": "on"
},
"time": 1667855195
}
},
"total": 1
}
If you want to work with one Note at a time, you can alternatively use the
note.get
request to retrieve
the next Note waiting in the Notefile.
{
"req": "note.changes",
"file": "data.qi",
"delete": true
}
J *req = NoteNewRequest("note.changes");
JAddStringToObject(req, "file", "data.qi");
JAddBoolToObject(req, "delete", true);
NoteRequest(req);
req = {"req": "note.changes"}
req["file"] = "data.qi"
req["delete"] = True
rsp = card.Transaction(req)
Resulting in the following JSON response:
{
"body": {
"command": "on"
},
"time": 1667855195
}
If no notes are available the note.get
request returns a {note-noexist}
error.
{ "err": "no notes available {note-noexist}" }
Both the note.changes
and note.get
requests allow to pass a delete
argument, which controls whether to delete Note(s) after you retrieve
them from the Notefile.
At this point you've now seen how to send commands to devices using the Notehub API, as well as how to receive those commands using the Notecard. To put everything together, let's look at an example of how you can use a host to receive commands from the Notecard, and then take action.
Receiving Commands on a Host
When implementing a command-and-control architecture you may want
to receive commands on a host microcontroller or single-board computer.
A host MCU makes it possible to receive commands and take action, for
example using commands like {"command":"on"}
and {"command":"off"}
as a
trigger for turning on and off a light, respectively.
We provide Notecard libraries that allow you to communicate with the Notecard on a wide variety of hosts.
In this section you'll see the steps you need to take using the Notecard libraries to implement a command-and-control architecture.
As an example, we'll show how to use the Notecard Arduino library running on a Blues Swan, but you can use the same steps to implement this architecture with any Notecard library and virtually any MCU.
Step 1: Configure Your Notecard
As a first step, remember that you must configure your Notecard so that
it can receive commands. You can do this either in the
In-Browser Terminal using the configuration below
(remembering to substitute YOUR_PRODUCTUID_HERE
) with your own value.
{
"req": "hub.set",
"product": "YOUR_PRODUCTUID_HERE",
"mode": "continuous",
"sync": true
}
Or you can set up your Notecard using one of the Notecard's libraries. For example, the code below shows how to set up a Notecard to immediately receive inbound Notes using the Notecard's Arduino SDK.
J *req = notecard.newRequest("hub.set");
JAddStringToObject(req, "product", "YOUR_PRODUCTUID_HERE");
JAddStringToObject(req, "mode", "continuous");
JAddBoolToObject(req, "sync", true);
notecard.sendRequest(req);
Step 2: Check for Inbound Notes
Once you have your Notecard set up, you next need to check for inbound notes.
The most common way to do this check is with the note.changes
or note.get
requests discussed in the previous section.
As another option, you can also configure the Notecard to use an interrupt
that allows you to use the ATTN
pin to wake your host when you receive
a new Note in a specified Notefile. If you're interested in that approach,
check out
Handling Notecard Interrupts.
Regardless of the approach you take, you'll need some logic that can parse
the command you need out of the Notecard's JSON response. The code below
shows an Arduino example that parses the note.get
request's JSON and returns
the command. (For example, if note.get
returns {"body:{"command":"on"})
this function returns "on"
).
char* getLastCommand()
{
// To hold "on" and "off". Increase the size if you need to send longer commands.
static char command[4];
J *req = notecard.newRequest("note.get");
JAddStringToObject(req, "file", "data.qi");
JAddBoolToObject(req, "delete", true);
J *rsp = notecard.requestAndResponse(req);
if (notecard.responseError(rsp)) {
notecard.logDebug("No notes available");
command[0] = '\0';
} else {
J *body = JGetObject(rsp, "body");
strncpy(command, JGetString(body, "command"), sizeof(command));
}
notecard.deleteResponse(rsp);
return command;
}
If you're using note.changes
you'll need some additional logic as the
request can return multiple Notes in one response.
Finally, if you're not using interrupts you need to check for inbound notes
in a loop. For our Arduino example this is straightforward as Arduino
provides a built-in loop
function. The code below checks for inbound
notes every second.
void loop()
{
char* command = getLastCommand();
// Use the command (which we'll do in the next step)
// Wait one second before looking for changes again
delay(1000);
}
Step 3: Take Action
As a last step, now that you have your command you can take project-specific actions. For example, this is where you may want to turn on/off a light, move a robot in a given direction, or shut off a piece of hardware.
As one example, the code below shows how to use {"command":"on"}
to turn
on the Swan MCU's built-in LED, and {"command":"off"}
to turn that same
light off.
char* command = getLastCommand();
if (!strncmp(command, "on", sizeof("on")))
{
notecard.logDebug("Turning light on");
digitalWrite(LED_BUILTIN, HIGH);
}
if (!strncmp(command, "off", sizeof("off")))
{
notecard.logDebug("Turning light off");
digitalWrite(LED_BUILTIN, LOW);
}
This example's full code is below for your reference. Remember that although this implementation uses Arduino and the Swan, you can perform the same steps using any of the Notecard's libraries, and you can run your logic on virtually any MCU.
Refer back to the section on sending commands to devices to learn how to send the necessary Notehub API requests to test this example.
#include <Notecard.h>
#define serialDebug Serial
#define productUID "YOUR_PRODUCTUID_HERE"
Notecard notecard;
void setup()
{
while (!serialDebug);
serialDebug.begin(115200);
notecard.begin();
notecard.setDebugOutputStream(serialDebug);
// Configure the Notecard
J *req = notecard.newRequest("hub.set");
JAddStringToObject(req, "product", productUID);
JAddStringToObject(req, "mode", "continuous");
JAddBoolToObject(req, "sync", true);
notecard.sendRequest(req);
// Initialize digital pin LED_BUILTIN as an output,
// and ensure the light starts off.
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
}
char* getLastCommand()
{
// To hold "on" and "off". Increase the size if you need to send longer commands.
static char command[4];
J *req = notecard.newRequest("note.get");
JAddStringToObject(req, "file", "data.qi");
JAddBoolToObject(req, "delete", true);
J *rsp = notecard.requestAndResponse(req);
if (notecard.responseError(rsp)) {
notecard.logDebug("No notes available");
command[0] = '\0';
} else {
J *body = JGetObject(rsp, "body");
strncpy(command, JGetString(body, "command"), sizeof(command));
}
notecard.deleteResponse(rsp);
return command;
}
void loop()
{
char* command = getLastCommand();
if (!strncmp(command, "on", sizeof("on")))
{
notecard.logDebug("Turning light on");
digitalWrite(LED_BUILTIN, HIGH);
}
if (!strncmp(command, "off", sizeof("off")))
{
notecard.logDebug("Turning light off");
digitalWrite(LED_BUILTIN, LOW);
}
// Wait one second before looking for changes again
delay(1000);
}