In this section we'll see Cloud Variables in action, using
libcanopy in a C program.
To use libcanopy you must include the header file:
#include <canopy.h>
Initialize libcanopy by creating a Canopy Context with canopy_init_context:
int main(void) {
CanopyContext ctx;
ctx = canopy_init_context();
if (!ctx) {
canopy_write_error(NULL, stderr, "Failed to create context.");
return -1;
}
The canopy_init_context routine will return NULL
if an error occurred. You can use canopy_write_error to
output details about any errors that may have occurred, along with your own
message about the error.
Configure the Context using canopy_set_opt:
CanopyResultEnum result;
result = canopy_set_opt(ctx,
CANOPY_DEVICE_UUID, "",
CANOPY_DEVICE_SECRET_KEY, ""
);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Failed to configure context");
return -1;
}
Replace the UUID and Secret Key above with those for your device. In
production, you should read these from a file or environment variable (more
on that later). The canopy_set_opt routine takes an arbitrary
number of KEY, VALUE parameter pairs.
Next, initialize a Cloud Variable with canopy_var_init:
result = canopy_var_init(ctx, "out float32 temperature");
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Failed to init cloud var 'temperature'.");
return -1;
}
The second parameter to canopy_var_init is the Cloud
Variable Declaration. This configures the variable's
direction, datatype and name.
The direction dictates who can modify the variable's value. It must be one of:
in - Only the cloud/app can modify. Useful for
receiving remote contol instructions.
out - Only the device can modify. Useful for reporting sensor data.inout - Both the device and cloud/app can modify.
Useful for device properties that can be changed both remotely and
within firmware.The datatype dictates the datatype of the Cloud Variable's value. It must be one of:
booldatetimefloat32float64int8int16int32stringuint8uint16uint32structtupleWe'll talk more about the various datatypes later.
The name is a name of your choosing for the Cloud Variable. By
convention, these are all lowercase with underscore(_) as a word seperator. For example "master_on_off".
You can change the value of a Cloud Variable using
canopy_var_set:
// Your code for reading temperature sensor goes here.
float t = ReadTemperature();
// Set the Cloud Variable's local value
result = canopy_var_set(ctx, "temperature", CANOPY_VALUE_FLOAT32(t));
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error setting 'temperature' to %f.", t);
return -1;
}
The canopy_var_set routine is generic and works for any
datatype. The second parameter must be a CanopyVarValue
object, which is why the CANOPY_VAR_FLOAT(t) call is needed.
Utility routines such as canopy_var_set_float32 can be used
instead and are slightly more convenient:
result = canopy_var_set_float32(ctx, "temperature", t);
The above code only changes the local value of
'temperature' in device memory. It does not actually
communicate with the cloud. In fact, there has been no interaction
with the cloud server at all so far. That happens next with
canopy_sync.
To report the value of 'temperature' to the Canopy Cloud Service (CCS), you must
call canopy_sync or canopy_sync_blocking:
// Synchronize with the cloud (10-second timeout)
result = canopy_sync_blocking(ctx, 10*CANOPY_SECONDS);
if (result == CANOPY_ERROR_TIMED_OUT) {
// Operation timed out
} else if (result != CANOPY_SUCCESS) {
// Other error occurred
}
The canopy_sync_blocking routine communicates with the
CCS using either WebSockets (default) or HTTP/REST. Because this is
the first time canopy_sync_blocking has been called, a whole
bunch of stuff happens:
ccs.canopy.link.'temperature' Cloud Variable is sent to the cloud.'temperature' is sent to the cloud.
temperature (if this is the first time that variable name
has been reported by this device), and stores its value in the database.
Subsequent calls to canopy_sync_blocking will use the open
WebSocket connection and do much less work:
canopy_sync* call.canopy_sync* call.
Typically, canopy_sync_blocking is called repeatedly in your
program's main loop. Whenever your program isn't doing anything else, it is
good practice to call canopy_sync_blocking.
You can use the canopy_once_every helper routine to
periodically read and report sensor data.
while (1) {
long reportTimer = 0;
// Synchronize with the cloud
result = canopy_sync_blocking(ctx, 10*CANOPY_SECONDS);
if (result == CANOPY_ERROR_TIMED_OUT) {
// Communication with cloud failed. Wait a few seconds, then try
// again.
canopy_write_error(ctx, stderr, "Error syncing");
sleep(10);
continue;
}
// Report temperature every 60 seconds
if (canopy_once_every(&reportTimer, 60*CANOPY_SECONDS)) {
// Your code for reading temperature sensor goes here.
float t = ReadTemperature();
// Update the Cloud Variable's local value
result = canopy_var_set_float32(ctx, "temperature", t);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error setting 'temperature' to %f.", t);
return -1;
}
}
}
Cloud Variables make it easy to obtain data from the cloud. The first step
is to initialize a Cloud Variable with in or
inout direction. For example, we can create an
in variable called led_on that can be remotely
controlled through the cloud.
result = canopy_var_init("in bool led_on");
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error initializing variable 'led_on'.");
return -1;
}
You can then use canopy_sync_blocking to fetch the latest
value from the cloud into the local copy.
result = canopy_sync_blocking(ctx, 10*CANOPY_SECONDS);
Finally, read the local copy using canopy_var_get:
bool led = false;
// Read the value of the "led_on" cloud variable
result = canopy_var_get(ctx, "led_on", CANOPY_READ_BOOL(&led));
if (result == CANOPY_ERROR_VARIABLE_NOT_SET) {
// This cloud variable has never been set.
// 'led' will not be modified, meaning the default of 'false' gets used.
} else if (result != CANOPY_ERROR_SUCCESS) {
canopy_write_error(ctx, stderr, "Error reading variable 'led_on'.");
return -1;
}
// Your code for turning on/off the LED goes here:
SetLED(led);
The canopy_var_get routine is generic and works for any
datatype. The second parameter must be a CanopyVarReader
object, which is why the CANOPY_READ_BOOL(&led) call is
needed.
Utility routines such as canopy_var_get_bool can be used
instead and are slightly more convenient:
result = canopy_var_get_bool(ctx, "led_on", &led);
You can also register a callback that gets triggered whenever a Cloud Variable changes.
result = canopy_var_on_change(ctx, 'led_on', HandleLEDChange, NULL);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error registering callback for 'led_on'");
return -1;
}
The third parameter, HandleLEDChange, is a function pointer to
a routine such as:
static int HandleLEDChange(CanopyContext ctx, const char *varName, void *userData) {
bool led;
result = canopy_var_get_bool(ctx, "led_on", &led);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error reading variable 'led_on'.");
return -1;
}
// Your code for turning on/off the LED goes here:
SetLED(led);
return 0;
}
The last parameter userData will match the last parameter
passed to canopy_var_on_change allowing you to pass arbitrary
data to the callback routine.
Registered callbacks only get triggered during the execution of
canopy_sync and canopy_sync_blocking, so be sure
to sync often.
#include <canopy.h>
#include <unistd.h>
static void SetLED(bool onOff) {
// Your code for turning on/off the LED goes here
if (onOff) {
printf("LED turned on\n");
} else {
printf("LED turned off\n");
}
}
static float ReadTemperature() {
// Your code for reading temperature sensor goes here
return 98.7f;
}
static int HandleLEDChange(CanopyContext ctx, const char *varName, void *userData) {
bool led;
result = canopy_var_get_bool(ctx, "led_on", &led);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error reading variable 'led_on'.");
return -1;
}
SetLED(led);
return 0;
}
int main(void) {
CanopyContext ctx;
CanopyResultEnum result;
ctx = canopy_init_context();
if (!ctx) {
canopy_write_error(ctx, stderr, "Failed to create context.");
return -1;
}
result = canopy_set_opt(ctx,
CANOPY_DEVICE_UUID, "",
CANOPY_DEVICE_SECRET_KEY, ""
);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Failed to configure context");
return -1;
}
result = canopy_var_init(ctx, "out float32 temperature");
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Failed to init cloud var 'temperature'.");
return -1;
}
result = canopy_var_init(ctx, "in bool led_on");
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Failed to init cloud var 'led_on'.");
return -1;
}
result = canopy_var_on_change(ctx, 'led_on', HandleLEDChange, NULL);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error registering callback for 'led_on'");
return -1;
}
// Loop forever, syncing as often as possible
while (1) {
long reportTimer = 0;
// Synchronize with the cloud
result = canopy_sync_blocking(ctx, 10*CANOPY_SECONDS);
if (result == CANOPY_ERROR_TIMED_OUT) {
// Communication with cloud failed. Wait a few seconds, then try
// again.
canopy_write_error(ctx, stderr, "Error syncing");
sleep(10);
continue;
}
// Report temperature every 60 seconds
if (canopy_once_every(&reportTimer, 60*CANOPY_SECONDS)) {
float t = ReadTemperature();
// Update the Cloud Variable's local value
result = canopy_var_set_float32(ctx, "temperature", t);
if (result != CANOPY_SUCCESS) {
canopy_write_error(ctx, stderr, "Error setting 'temperature' to %f.", t);
return -1;
}
}
}
return 0;
}