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:
bool
datetime
float32
float64
int8
int16
int32
string
uint8
uint16
uint32
struct
tuple
We'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; }