Callbacks and Event handling

What are callbacks?

Electric UI uses a data-driven model where message payloads contain data representing something like a control input or sensor reading. The general model relies on a publish/subscribe scheme where data is communicated asynchronously to code that consumes the variables.

But for something event driven such as saving a series of values to persistent storage, triggering a move to a bookmarked position with a robot, or requesting a system reboot, its not ideal to poll a flag for a change, so we want the ability for Electric UI to call the relevant function directly.

How do I get a UI to call a function?

Embedded Setup

We use a TYPE_CALLBACK packet which contains a function pointer. In C, declare your function in your header ( or private functions in the same scope as eUI ) and then provide a tracked variable with a pointer to it.

c
void motor_reset_callback();
// in your .c file
eui_message_t dev_msg_store[] =
{
EUI_FUNC( "reset", motor_reset_callback ),
};
void motor_reset_callback()
{
//disable the motor
set_servo_enable( false );
//return to home position
request_servo_position( 0 );
}

When inbound messages arrive for the reset message, Electric UI will call your function.

If you aren't using the variable helper macros while adding the structure to the tracked array, use TYPE_CALLBACK, and the ptr union's callback member stores the pointer to your function. The size doesn't matter as pointer data is never transmitted, but we use a size of 1.

User Interface

We'll add a button to our UI which calls the callback once when pressed.

tsx
import { Button } from '@electricui/components-desktop-blueprint'
tsx
// In the layout somewhere, make a button to invoke the callback on press
<Button callback="reset">Reset Motor Position</Button>

Callbacks when any variable is called?

This is done a little indirectly. electricui-embedded supports a configurable callback which fires every time a message comes in. Inside this callback, its possible to write logic which calls functions after Electric UI has finished processing the inbound packet.

This can be useful if you want to execute logic on the microcontroller after variables are set. This might be used if you have some digitally addressed LEDs and want to immediately handle the data, or the UI just sent some changes to a sensor configuration which need to be written to hardware registers.

We generally caution the over-use of UI callbacks, as in-experienced users may tie embedded logic to the UI operation.

Strong coupling to user interface behaviour is a (architectural) design smell, as your embedded device might not always have a UI connected, and the interface could change behaviour later down the track.

Interface callback setup

Lets start by making a new function which will become our callback.

c
void eui_serial_callback( uint8_t message )
{
switch(message)
{
case EUI_CB_TRACKED:
// UI recieved a tracked message ID and has completed processing
break;
case EUI_CB_UNTRACKED:
// UI passed in an untracked message ID
break;
case EUI_CB_PARSE_FAIL:
// Inbound message parsing failed, this callback help while debugging
break;
}
}

This callback is provided to the library as part of the eui_interface_t which holds the output function and parser state.

This can be done where the data output function callback is configured.

c
comm_interface.output_cb = &uart_write;
comm_interface.interface_cb = &eui_serial_callback;

If you have multiple interfaces, or streamlined for compile-time setup, there is an interface declaration macro EUI_INTERFACE_CB which can be used.

c
eui_interface_t comm_links[] = {
EUI_INTERFACE( &uart_write ),
EUI_INTERFACE( &websockets_write ),
};
// becomes
eui_interface_t comm_links[] = {
EUI_INTERFACE_CB( &uart_write, &eui_serial_callback ),
EUI_INTERFACE_CB( &websockets_write, &eui_ws_callback ), // call the same callback
};

In the case where multiple interfaces are used, you can put an interface callback on just one, use the same callback for many, or have unique callbacks per interface. Generally we prefer a unique callback per interface as it allows for custom behaviour with special transports.

The general behaviour of the callback during execution is:

  1. Electric UI catches a valid inbound packet, and is ready to process the data.
    • If the packet fails in the parser (CRC error, etc), the callback fires with the EUI_CB_PARSE_FAIL flag.
  2. If its a tracked variable, any writes or function callbacks will occur, and ack or query responses are sent back.
    • If its an untracked variable, nothing happens here.
  3. The interface callback is fired.
  4. The inbound packet buffers are erased, ready for the next packet.

Interface callback usage

Now we have our interface callback firing, we'll explore how to call functions or manipulate data based on the inbound packet.

We'll consider what happens when we get the callback with EUI_CB_TRACKED flag. In this case, lets pretend we are receiving a packet with an identifier of rgb, holding the red, green and blue colour values for a multi-coloured LED.

In this situation, Electric UI has already copied the inbound data to the tracked variable, but we want to call the LED library with a update_leds() to actually send this new value to the hardware.

First, lets see how this is written, then we'll breakdown what's happening and what other data can be accessed.

c
void eui_serial_callback( uint8_t message )
{
switch(message)
{
case EUI_CB_TRACKED:
{
// UI recieved a tracked message ID and has completed processing
uint8_t *msg_id = comm_interface.packet.id_in;
// See if the inbound packet name matches our intended variable
if( strcmp( (char *)msg_id, "rgb" ) == 0 )
{
// the packet was for the rgb tracked variable
update_leds();
}
}
break;
case EUI_CB_UNTRACKED:
// UI passed in an untracked message ID
break;
case EUI_CB_PARSE_FAIL:
// Inbound message parsing failed, this callback help while debugging
break;
}
}
  1. In the callback, we use a switch on the message flag, and when the tracked message flag is used we run our code.
  2. We create a pointer to the message identifier string in the packet which just came in, letting us check if the message identifier matches rgb with strcmp.
  3. If the message matches, we call our update_leds() function, which will update our LED with the data managed by our tracked variable.

The interesting line here is really the uint8_t *msg_id = comm_interface.packet.id_in;, as we are reaching into the inbound packet's buffer and grabbing what we need.

For situations where the message isn't tracked, grabbing data might also be useful, and this is also pretty easy, as the packet's header, and inbound buffer of data are also available.

c
eui_header_t header = comm_interface.packet.header;
uint8_t *name_rx = comm_interface.packet.id_in;
void *data = comm_interface.packet.data_in;
if( strcmp( (char *)msg_id, "sensor_cal" ) == 0 && header.type == TYPE_UINT16 )
{
// Got our calibratiion packet, with a uint16 payload
uint16_t calibration_value = *(uint16_t*)data;
do_something_fancy( calibration_value );
}
// Other data available in the header includes the
bool packet_was_query = header.response;
uint16_t packet_size = header.data_len; // how large the header says the payload is
uint16_t buffered_data_size = comm_interface.data_bytes_in; // buffered data - 1kB max

Have a look at the electricui-embedded API Reference for more details.