2.2 Cloud Variables - Cloud Variables with libcanopy

In this section we'll see Cloud Variables in action, using libcanopy in a C program.

Initializing libcanopy and Cloud Variables

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:

The datatype dictates the datatype of the Cloud Variable's value. It must be one of:

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".

Reporting to the Cloud

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:

Subsequent calls to canopy_sync_blocking will use the open WebSocket connection and do much less work:

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;
            }
        }
    }

Obtaining Data From The Cloud

Using canopy_get_var

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);

Obtaining Data From The Cloud Using Callbacks

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.

Putting It All Together

Here is a complete example:
#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;
}