How to write an application using the Thingsquare SDK

This document will go through writing an application binary using the Thingsquare SDK.

What is a firmware application

An app, or application, is within this document referring to an app written with the Thingsquare firmware SDK, to be included in a firmware binary image, that is flashed to a hardware device. It will define the look and feel of your product, as well as the functionality.

The complete firmware binary image consists of the following,

  • Thingsquare firmware SDK libraries
  • The app() function
  • Thingsquare callback
  • Thingsquare modules
  • Actuator drivers
  • Sensor drivers
  • Name
  • Other business logic

Not all of these are used for every binary - for example, some products do not make use of any Thingsquare firmware modules, and others do not have any proprietary drivers but the modules are sufficient. The app() function is mandatory however.

Every type of device needs particular applications and drivers (where applicable) in order to define and handle the functionality and look and feel.

For example, an in-door environment product needs the drivers for the air properties sensor (humidity, temperature, and so on), and an application that defines how often these are read, what LEDs to blink to indicate what, and so on. Another product might be a smart light fixture control unit. That product would need drivers for power management and light control, and sensor drivers for temperature. The application would typically utilize the Thingsquare lighting module, which gives easy zone control and scheduling, and be a small layer defining how to read out temperature and monitor power supply status.

Components

Thingsquare firmware SDK libraries

The Thingsquare firmware SDK includes the necessary libraries and protocols for a device to be a fully-functioning, high-performant device in a mesh network and handle a range of tasks. The tasks include conserving power and radio utilization, periodic health statistics, information flow with the backend servers, firmware updates, and many more.

You as an application designer do not have to do anything with these parts. They are automatically included in the compiled binary image and runs automatically on bootup.

The app() function

Every product firmware needs an application entry point. This is the void app(void) function, which must exist even if it is empty.

The app() is responsible for starting whatever drivers and other applications the product needs to be what it should be. app() is called once, when the device starts up, after the basic hardware and firmware initialization has taken place. At this point in time, the hardware system clocks and peripherals (UART, LEDs) as applicable have been initialized. The network stack and the Thingsquare default modules have started and the device has begun finding network connectivity.

Now, when app() runs, it may initialize sensors, set up callbacks and start Thingsquare modules as necessary. It will not be run again, so any further applications need to be started from here.

Here is a very simple app.

#include "thsq.h"
/*---------------------------------------------------------------------------*/
void
app(void)
{
  printf("I'm completely operational and all my circuits are functioning perfectly\n");
}
/*---------------------------------------------------------------------------*/

Here is a slightly larger app.

#include "thsq.h"

PROCESS(app_process, "App Process");
/*---------------------------------------------------------------------------*/
void
app(void)
{
  thsq_set_platform("acme-corp-hygrow-garden-sensor-868");
  process_start(&app_process, NULL);
}
/*---------------------------------------------------------------------------*/
PROCESS_THREAD(app_process, ev, data)
{
  PROCESS_BEGIN();
  printf("Starting application.\n");

  while(1) {
    static struct etimer et;
    static int counter;

    printf("Still running: %d\n", counter);
    counter++;
    etimer_set(&et, CLOCK_SECOND);
    PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));
  }
  PROCESS_END();
}
/*---------------------------------------------------------------------------*/

Thingsquare callback

Thingsquare callbacks are described in detail in other parts of the documentation. The callbacks are the best way for the application to be notified of information flow (variable updates, commands) and events from Thingsquare modules.

It is not necessary to implement such a callback, and there can be more than one callback. For example, a driver may have a callback in order to listen for commands or variables. However, to separate concerns and keep the high-level logic from low-level drivers, it is better for the application callback to listen for the appropiate variables and commands and use the drivers accordingly.

Here is example code of how a callback is included in the application.

#include "thsq.h"
/*---------------------------------------------------------------------------*/
static void
callback(enum thsq_reason r, const char *str, int len)
{
  /* here, we do something */
}
/*---------------------------------------------------------------------------*/
void
app(void)
{
  static struct thsq_callback cb;
  thsq_add_callback(&cb, callback);
}
/*---------------------------------------------------------------------------*/

Thingsquare modules

The Thingsquare firmware SDK comes with a set of modules. They are documented in detail in other parts of the documentation. Mostly, initializing and use of a module is as simple as calling the respective init() function and either implement a specific callback or use the Thingsquare callback, as required by the module.

For example, this will use the GPIO-module to listen for events on a input-pin.

#include "thsq.h"
/*---------------------------------------------------------------------------*/
static void
callback(enum thsq_reason r, const char *str, int len)
{
  if(r == THSQ_GPIO_INPUT) {
    /* we do something here */
  }
}
/*---------------------------------------------------------------------------*/
void
app(void)
{
  static struct thsq_callback cb;
  thsq_add_callback(&cb, callback);

  /* use GPIO module, and IOID_30 as input pin, pull-up resistor enabled */
  thsq_gpio_init();
  thsq_gpio_set_default("------30U");
}
/*---------------------------------------------------------------------------*/

Sensor and actuator drivers

Many products will control and sense physical things and phenomena. The drivers for this should be initialized and set to a good default state from the app(). How to write drivers and integrate them in the build is described in detail in other parts of the documentation.

For this example, this product has a hypothetical ABC123 humidity sensor that is sampled periodically (see documentation on Thingsquare callbacks).

#include "thsq.h"
/*---------------------------------------------------------------------------*/
static void
callback(enum thsq_reason r, const char *str, int len)
{
  if(r == THSQ_GPIO_INPUT) {
    /* we do something here */

  } else if(r == THSQ_PERIOD) {
    int sample;
    if(abc123_sample(&sample) >= 0) {
      thsq_sset("rh", sample);
      thsq_push();
    } else {
      /* handle error */
    }

    /* go back to sleep after sample */
    abc123_powerdown();
  }
}
/*---------------------------------------------------------------------------*/
void
app(void)
{
  static struct thsq_callback cb;
  thsq_add_callback(&cb, callback);

  /* use GPIO module, and IOID_30 as input pin, pull-up resistor enabled */
  thsq_gpio_init();
  thsq_gpio_set_default("------30U");

  /* ABC123 should sleep by default */
  abc123_init();
  abc123_powerdown();
}
/*---------------------------------------------------------------------------*/

Hardware-defined overrides

Differences in the reference hardware platforms (eg Texas Instruments CC1310 Launchpad) to the custom product hardware under development might mean overriding default behavior or pinouts is necessary.

External flash chip

The SDK supports a set of hardware platforms, mostly defined by what microcontroller it is using (CC1310, CC1350, CC2650, CC2538). For the CC13xx and CC26xx SOCs, a serial flash chip is necessary to enable firmware updates over the air. If this is not available on a platform, this can be disabled through a bootloader parameters call. Below, we will see an example of how the SPI pins to an external flash is overriden.

LEDs

It is common to use LEDs to indicate to a human user eg error conditions by flashing a red LED, or a fix green LED when everything is within the normal operating envelope. LEDs can also be used internally, visible to service technicians only by dismantling the product enclosure. Below we will see how the LEDs pins are overriden using init_leds().

UART

The firmware by default allows the use of printf() over UART. The output from the system itself is zero or minimal to conserve power and minimize information leaks that can be crucial for IP protection when the products are in the field. If the UART is not in use whatsoever, the initialization can be overriden so that the UART is never used. Below we will see how the UART is disabled using init_uart().

Boot-up sequence

The device bootup sequence is described in other parts of the documentation, however we will quickly touch on some parts here.

The boot-up sequence in brief looks something like this,

  1. bootloader
  2. external flash chip configuration
  3. hardware lowest level
  4. init_leds()
  5. clocks, watchdog, processes
  6. init_radio()
  7. init_uart()
  8. OS clocks
  9. protocol stack
  10. init_sensors()
  11. Thingsquare default libraries
  12. app()

The first thing that happens is hardware initialization and basic set up. The external flash chip, where applicable, is used very early in the boot process by the firmware update functionality. It checks for newer firmware binaries in the external flash before continuing with the boot. Then, the rest of the hardware is initialized: clocks and power management is set up, timers are started for the clocks etc.

LEDs

LED hardware are initialized early. If the user application implements init_leds(), this will be used instead. init_leds() may be used to override default behavior, to change the pins for the LEDs, or disable them.

Radio

Normally, radio initialization and configuration is handled completely by the system and a user application does not have do a thing. However, there can sometimes be reasons to change a configuration option, most often due to how the hardware is designed. This is generally done in the init_radio() call that can be overriden. Examples on what can be done is to enable external PA/LNA, change pins to or disable on RF switch.

For example, the cc1350 is dual-interface (sub-GHz and 2.4 GHz) and as such needs two antennas. On the Launchpad, it uses an RF switch to set what antenna to direct the energy towards, but on the cc1350 sensortag it instead uses a single-ended design without a switch. This is handled in the init_radio() call. The default is to follow the cc1350 Launchpad design.

UART

A little bit later, but still very early, the UART for serial output is initialized. If the user application implements init_uart(), this will be used instead. This can be used to change pins or disable serial output.

Sensors

Just before the Thingsquare libraries and modules are started, the init_sensors() is called. A user application is free to implement this and add functionality here.

Example

This example application changes the pinout for the external flash SPI, LEDs, and UART. The way to redefine the external flash pins is different since this is also handled by the bootloader very early.

#include "thsq.h"
#include "dev/cc26xx-uart.h"
#include "dev/leds-arch.h"
#include "ti-lib.h"
/*---------------------------------------------------------------------------*/
/* redefine the SPI pins for the external flash: miso, mosi, clk, cs */
BOOTLOADERPARAMS_OVERRIDE_XMEM_CONF(IOID_27, IOID_28, IOID_29, IOID_30);
/*---------------------------------------------------------------------------*/
void
init_leds(void)
{
  /* set the red, green, and blue LED pins; we have no blue LED */
  leds_arch_set_pins(IOID_13, IOID_12, IOID_UNUSED);
}
/*---------------------------------------------------------------------------*/
void
init_uart(void)
{
  /*
   * set UART pins: tx, rx, cts, rts
   * cts and rts are not used
   * Note: if this function was empty, the UART would instead be disabled
   */
#define UART_TX IOID_8
#define UART_RX IOID_7
  cc26xx_uart_init(UART_TX, UART_RX, IOID_UNUSED, IOID_UNUSED);
}
/*---------------------------------------------------------------------------*/
void
app(void)
{
  /* here, we do something, eg init drivers, start our applications */
}
/*---------------------------------------------------------------------------*/

Name

Each product must have a name that uniquely indicates the device. This is used as an identifier so that a device will not be able to perform a firmware update with a firmware binary that is incompatible with the hardware. Note that this is different from the TARGET name used when compiling the binary.

Use the void thsq_set_platform(const char *platform); call to set the platform name. Just set it once in the app, like in the following example. The example would be a sub-GHz garden sensor meant for the European market (since it indicates 868 MHz).

#include "thsq.h"
/*---------------------------------------------------------------------------*/
void
app(void)
{
  thsq_set_platform("acme-corp-hygrow-garden-sensor-868");
}
/*---------------------------------------------------------------------------*/

The platform name is used for firmware update purposes, and is reflected in the d variable platform. It can also be used by eg smartphone apps to change the user interface, eg by giving it a specific icon, or presenting the user with controls only available on that platform.

Other business logic

Apart from looking at just the functionality, there are many other things that define a thing as your product. For example, the use of LEDs as indicators, user buttons and input devices, response times, and so on. How it reacts to commands from the backend servers (see Information flow documentation), what commands are available, etc. How should the device react when for example temperature is out of a specified range, or when it has returned to normal conditions again.

How to start

So how do I start developing my product?

Start by taking any of the example files that came with the SDK, or copy-paste examples from the documentation.

If necessary, adapt to your hardware by redefining hardware pins as described above.

Give it a platform name using thsq_set_platform().

Build stubs of setting and reading variables and commands as necessary using callbacks.

Build stubs for drivers and use them from your application.

Fill out drivers and tweak the application.