Example 2 - BLINK LED Remotely

Introduction

Our goal is to expand the simple Hello World() application to control the LED remotely via REST API. We are going to expand the initial project with the following user functionality:

  • Turn OFF LED

  • Turn ON LED for a defined time

  • Trigger LED blinking for a defined time with a defined period

  • Get current LED state

To provide the new functionality we need to expand our application by the following items:

  • Defining the Custom Over-the-air APP Protocol

  • Handle UWB Payload Reception

  • Implements LED Functions - apart from low-level GPIO LED commands we will expand our knowledge about:

    • How to send UWB Data to the server

    • How to use application timer


1) Defining the Custom Over-the-air APP Protocol

In order to control LED remotely, we need to define messages between REST API on the server-side and Tag firmware.

All multi bytes data types within the app protocol are in Little-endian if not mentioned explicitly different.

Action ID Macros

Let’s start with ID macros in app_led_control.c.

#define SET_LED_OFF_ID      0x00
#define SET_LED_ON_ID       0x01
#define SET_LED_BLINK_ID    0x02
#define GET_LED_STATE_ID    0x10
  • Since JSON is too chatty for over-the-air communication, we have chosen a binary TLV message format.
  • Values in payload are represented in HEX format - so i.e our ID 4096 (DEC) is 0x1000 (HEX).

bc-request (1B)

type (2B)

length [B] (1B)

value (up to 100B)

0x64

APP_ID - 0x1000

length of value in bytes

depend on specific request - action ID + parameters

Turn OFF the LED

  • This is the simplest request of our application. The value part of TLV can only contain the action ID, which will be 0x00 in this case.

  • Let’s create a Turn OFF LED request, we would define the following structure:

  • Our request LED OFF can be transferred within a single byte, yet still, we need to encapsulate to predefined TLV structure bc-request, type, and length.

bc-request (1B)

type (2B)

length [B] (1B)

value (1B)

0x64

APP_ID - 0x1000

0x01

0x00 - turn_of_led

  • Resulting byte stream that will be sent from the server to the tag as a backchannel payload:

    • “6400100100”

  • Let’s define the structure for the parse of request parameters part. It will be only used inside our application, so it can be defined in “app_led_control.c”

typedef struct __attribute__((packed, aligned(1))){
uint8_t action_id;
} set_led_off_params_t;

Turn On the LED For a Defined Time

  • The second request, which will be used for the turning LED on, should have also other parameters. It will define the time of LED on the state. Once this time elapses, the application should turn off the LED automatically. The ID of this request will be 0x01. Let’s define the LSB of time 10ms and length of time field to 2B.

  • The request will have the following structure:

bc-request (1B)

type (2B)

length [B] (1B)

value (3B)

action ID

time [10ms] (2B)

0x64

APP_ID - 0x1000

0x03

0x01

time value - example (5s)
0x01F4

  • Resulting byte stream that will be sent from the server to the tag as a backchannel payload:

    • “6400100301F401”

  • Now we can define the structure for the parse of request parameters part. It will be only used inside our application, so it can be defined in “app_led_control.c”

typedef struct __attribute__((packed, aligned(1))){
uint8_t action_id;
uint16_t time;
} set_led_on_params_t;

Triggered LED Blinking

  • The third request will be similar to the previous request, so it will contain the time in the same units (LSB=10s) with 2B length and it must contain the period of blinking. It can have the same form as the time. Action ID of it will be 0x02.

  • This request will have the following structure:

bc-request (1B)

type (2B)

length [B] (1B)

value (3B)

action ID

time [10ms] (2B)

period [10ms] (2B)

0x64

APP_ID - 0x1000

0x05

0x02

time value - example (5s)
0x01F4

period value - example (200ms)
0x0014

  • Resulting byte stream that will be sent from the server to the tag as a backchannel payload:

    • “6400100502F4011400”

  • Now we can define the structure foLet’s have the action ID of this request 0x10.

The request will have the following structure:

bc-request (1B)

type (2B)

length [B] (1B)

value (1B)

action ID

0x64

APP_ID - 0x1000

0x01

0x10

  • Resulting byte stream that will be sent from the server to the tag as a backchannel payload:

    • “6400100110”

  • Let’s define the structure for the parse of request parameters part. It will be only used inside our application, so it can be defined in “app_led_control.c”

typedef struct __attribute__((packed, aligned(1))){
uint8_t action_id;
} get_led_state_params_t;

2) Receive UWB Payload

The payload is received within the callback function right after the over-the-air transmission between Tag and Anchor is over.

The callback function contains the structure app_data_t, which has the following definition:

typedef struct __attribute__((packed, aligned(1))){
    uint16_t app_id;
    uint8_t len;
    uint8_t data[100];
}app_data_t;
  • Now we need to parse the received data.

  • First, we need to check if some request was received during this period. We can do it using “len” member in the app_data_t structure which is a parameter of our entry function:
    app_t app_led_control(app_data_t* input_data).

  • If the “len” is 0, then no new data was received. In this case, we can escape our app with no action, because it only should do something when the request is received.

  • In other cases, we just parse the data field of input data. As we know from the previous step, the action ID is always the first byte of the data part of the received request. Based on it we can branch the code based on the required action.

  • For very simple actions like turn_off_LED and get_LED_state, we don’t need to parse any other parameters and we can directly call functions that execute the requested action.

  • For the other request that contains other parameters, we need to parse it and pass parsed parameters to the function, that execute the requested action. We can use the structures that we defined in the previous section.

  • Let’s put it all together. So the final code that checks if some bc request was received, parse received the request, and calls the function that executes the requested action can look like this:

app_t app_led_control(app_data_t* input_data)
{
    if(input_data->len == 0)
        return 0;
    else
    {
        uint8_t action = input_data->data[0];

        switch(action)
        {
            set_led_on_params_t* params_led_on;
            set_led_blink_params_t* params_led_blink;
            uint8_t led_state = 0xFF;

            case(SET_LED_OFF_ID):
                set_led_off();
                break;

            case(SET_LED_ON_ID):
                params_led_on = (set_led_on_params_t*)input_data->data;
                set_led_on(params_led_on->time);
                break;

            case(SET_LED_BLINK_ID):
                params_led_blink = (set_led_blink_params_t*)input_data->data;
                set_led_blink(params_led_blink->time, params_led_blink->period);
                break;

            case(GET_LED_STATE_ID):
                get_led_state(&led_state);
                break;

            default:
                unknown_action_id();
                //unknown request
                break;
        }
    }
  return 0;
}



3) Implements LED Functions

Let’s include relevant header files

  • “peripheral.h” - implements low-level board specific LED ON/OFF/TOGGLE functions
  • “app_rtc_timer.h” - implements application timer

set_led_off(void)

  • This function turns off the LED. Some other functions of our APP will use the application timer so, the set_led_off() function also has to discard all ongoing timers slots.

  • We also should save the new led state into the static variable led_state that will be defined in our C-file.

static void set_led_off(void)
{
    //discart all pending timer events from this application
    discard_timer_app_slots(APP_LED_CONTROL_ID);
    LED1_OFF();
    led_state = SET_LED_OFF_ID;
}

set_led_on(uint16_t time)

  • This function turn on the LED and sets the application timer to turn off the LED after a defined time.

  • Firstly let’s discard other pending timer operations. discard_timer_app_slots(uint16_t app_id)

  • Now let’s set the application timer via function app_timer_set_timeout(uint32_t time_us, uint16_t app_id, void* event_handler).

    • The input parameter time of function set_led_on() represents time in a unit of 10 ms. It must be converted to the microseconds (which is the unit required by the app_timer function). In addition to time, we must pass our APP_ID, and callback function that will be called once the time elapsed. For this purpose, we can use function set_led_off().

Finally, we can turn on the LED using the function LED1_ON().

We also should save the new led state into the static variable led_state.

static void set_led_on(uint16_t time)
{
    uint32_t time_us;
    
    //discart all pending timer events from this application
    discard_timer_app_slots(APP_LED_CONTROL_ID);

    //convert time from 10ms units to us and set app_timer with set_led_off() as callback.
    time_us = (uint32_t)time * 100000;
    app_timer_set_timeout(time_us, APP_LED_CONTROL_ID, set_led_off);

    LED1_ON();
    led_state = SET_LED_ON_ID;
}

set_led_blink(uint16_t time, uint16_t period)

  • Similar to the previous functions, we must at first discard all pending timer events from our application. Then we must convert time and period from unit 10ms to the microseconds.

  • Now we can set a timeout of blinking similarly to the function set_led_on().

  • In this case, we also must define other functions that will be used as a callback for app_timer and will ensure LED blinking, let's call it led_blink_run(). We need to define a static variable out of the function where we will store half of the blinking period. It will be used  led_blink_run() to set the time of the current state of the LED. This function only toggles the LED state and registers itself as a callback function to app_timer with a time of half period of blinking.

We also should to save new led state into static variable led_state.

static uint32_t blink_half_period;

static void led_blink_run()
{
    LED1_TOGGLE();
    app_timer_set_timeout(blink_half_period, APP_LED_CONTROL_ID, led_blink_run);
}

static void set_led_blink(uint16_t time, uint16_t period)
{
    uint32_t time_us;
    uint32_t period_us;

    //discart all pending timer events from this application
    discard_timer_app_slots(APP_LED_CONTROL_ID);

    //convert time from 10ms units to us and set app_timer with set_led_off() as callback.
    time_us = (uint32_t)time * 100000;
    app_timer_set_timeout(time_us, APP_LED_CONTROL_ID, set_led_off);
    
    //convert period from 10ms units to us.
    period_us = (uint32_t)period * 100000;
    blink_half_period = period_us >> 1;

    led_blink_run();
    
    led_state = SET_LED_BLINK_ID;
}

get_led_state(uint8_t* state)

  • This function sends the current state of the LED to the server. Let’s have the responses in the same format as requests with the only difference, that set time will be replaced with the remaining time.

  • Firstly we need to define the response array. Let’s define the length of the array as the length of the longest request which is the set_led_blink request. The universal message type ID (0x64) and APP ID will be added to the response automatically by the lower layer of FW, so the response body must only contain the LED state, remaining time (only if LED is ON or blinking), and period (only if LED is blinking). Do not forget to convert the period and time from microsecond to the 10ms unit.

  • To send a prepared response to the server, we can use the function that is declared in app_data_handlig.h
    bool Data_Req(uint16_t app_id, uint8_t len, uint8_t* data, uint8_t priority, data_confirm_cb_t confirm_cb). We only pass the application ID, response length, and response body to this function, which stores it into the data request buffer and it will be sent to the server as a payload in the UWB blink. There is also a need to specify the priority for sending the message. Priorities are defined in apps_defs.h, let's use USER_LOW_PRIORITY for this function.

  • We can also pass the callback function, which would be called once the data request is done. It can be used to set the flag, which confirms that the data request is done (the data was sent to the server). To keep this application as simple as possible, this feature will not be used. Thus, let’s pass NULL instead of the callback function as a parameter to the Data_Req().

static void get_led_state(uint8_t* state)
{
    uint8_t response_body[sizeof(set_led_blink_params_t)];
    uint8_t response_length = 0;

    if(led_state == SET_LED_OFF_ID)
    {
        response_body[0] = led_state;
        response_length = 1;
    }
    else if(led_state == SET_LED_ON_ID)
    {
        set_led_on_params_t parameters;

        parameters.action_id = led_state;
        //get remaining time and convert it to 10ms unit
        parameters.time = get_remaining_time_ms(timeout_slot, APP_LED_CONTROL_ID)  / 10;    

        memcpy((void*)response_body,(void*)&parameters, sizeof(parameters));
        response_length = sizeof(parameters);
    }
    else if(led_state == SET_LED_BLINK_ID)
    {
        set_led_blink_params_t parameters;

        parameters.action_id = led_state;
        //get remaining time and convert it to 10ms unit
        parameters.time = get_remaining_time_ms(timeout_slot, APP_LED_CONTROL_ID)  / 10;   
        
        //half period is stored in us, convert it to 10ms units and multiple it by 2 to get full period 
        parameters.period = blink_half_period * 2 / 10000;                                   

        memcpy((void*)response_body,(void*)&parameters, sizeof(parameters));
        response_length = sizeof(parameters);
    }
    //put the response into the response buffer
    Data_Req(APP_LED_CONTROL_ID, response_length,response_body,USER_LOW_PRIORITY,NULL);
}

unknown_action_id(void)

  • Let’s try to implement a bit of error handling. The last function sends an error response in case of the action-id in the received request is unknown. This function only prepares a response with a predefined error code which will be sent to the server. Let’s have the error code of this error = 0xFF.

#define ERR_UNKNOWN_ACTION_ID   0xFF  

static void unknown_action_id()
{
    uint8_t response_body = ERR_UNKNOWN_ACTION_ID;
    uint8_t response_length = 1;
    
    Data_Req(APP_LED_CONTROL_ID,response_length,&response_body, USER_LOW_PRIORITY,NULL);
}

Congrats you just make your first backchannel application on the tag side.

Here you can find complete files that we have created:

app_led_control.happ_led_control.c

Now you can build the code and load it into the tag.



4) Let's Blink via RTLS API

The easiest way to do it is to use the interactive API where you can send arbitrary payload:

  • http://[RTLS Studio IP]/documentation/api-rtlsmanager

  • Please fill in the MAC address of your tag into the id or mac label.

  • Then you can put your request payload i.e “6400100502F4011400” as defined in our application protocol in section 1.

  • Parameters timeout and anchor are optional.

  • You can find more info about over-the-air messaging on page UWB Data Exchange.


On this page: