C Library API

Covers the electricui-embedded C library API surface. Headers/source has embedded Doxygen documentation which is a superset of this document.

Interfaces

Electric UI's embedded library allows multiple communication transports to be used simultaneously.

A eui_interface_t contains storage for an inbound packet (buffers and decoder state information) along with a pointer which is used to write data over said transport.

An optional callback function can be used for additional operational information.

typedef struct {
eui_packet_t packet;
callback_data_out_t output_cb;
callback_uint8_t interface_cb;
} eui_interface_t;

Initialisation

Instantiate a single interface verbosely, or using helper macros:

// Set the output function in the eui_interface_t declaration
eui_interface_t serial_comms = { .packet = {0},
.output_cb = &output_callback,
.interface_cb = 0 };
// We provide macros to make this cleaner
eui_interface_t serial_comms = EUI_INTERFACE( &output_callback );
eui_interface_t serial_comms = EUI_INTERFACE_CB( &output_callback, &eui_parser_callback );

Many interfaces in an array:

eui_interface_t comms_interfaces[] = {
EUI_INTERFACE( &serial_write ),
EUI_INTERFACE( &websockets_write )
};

We recommend using enum indices for ergonomic access when the language spec allows:

enum {
EUI_INTERFACE_SERIAL = 0,
EUI_INTERFACE_WEBSOCKETS,
EUI_NUM_INTERFACES
} _COMMS_INTERFACES;
eui_interface_t comms_interfaces[EUI_NUM_INTERFACES] = {
[EUI_INTERFACE_SERIAL] = EUI_INTERFACE( &serial_write ),
[EUI_INTERFACE_WEBSOCKETS] = EUI_INTERFACE( &websockets_write )
};

It is also possible to perform the callback setup at run-time:

eui_interface_t serial_interface;
int main()
{
// ...
serial_interface.output_cb = &serial_write_cb; // required
serial_interface.interface_cb = &serial_parser_cb; // optional
// ...
}

If you look at the EUI_INTERFACE macro definition, the packet member of the eui_interface_t is instantiated verbosely through another macro EUI_PACKET_EMPTY rather than {0}.

{ .packet = EUI_PACKET_EMPTY, .output_cb = OUTPUT_PTR, .interface_cb = 0 }

This is due to subtle differences in compiler support for static-initialisation of sub-member structs with = {0} and prevents warning: missing braces around initializer [-Wmissing-braces] is thrown.

Setting the default interface

The library needs to be provided with a pointer to your declared interface(s), and with multiple interfaces it's assumed the pointer is to an array of eui_interface_t along with a valid length.

While not strictly necessary, providing these pointers at startup allows developer code to call a eui_send() function with no prior communication.

This setup occurs at run-time from one of your initialisation functions ideally, as the data is 'owned' by the firmware developer's scope. Relevant electricui-embedded functions:

void eui_setup_interface( eui_interface_t *link );
void eui_setup_interfaces( eui_interface_t *link_array, uint8_t link_count );

Used like so:

eui_interface_t serial_interface = EUI_INTERFACE( &serial_write_cb );
int main()
{
// ...
eui_setup_interface( &serial_interface );
// Or for an array of interfaces
eui_setup_interfaces( &comms_interfaces, EUI_NUM_INTERFACES );
// ...
}

Multiple interface setup can also be achieved with the EUI_LINK() macro, shown for reference:

#define EUI_LINK( INTERFACE_ARRAY ) ( setup_interfaces(INTERFACE_ARRAY, EUI_ARR_ELEM(INTERFACE_ARRAY)) )
eui_interface_t comms_interfaces[] = {
EUI_INTERFACE( &serial_write ),
EUI_INTERFACE( &websockets_write )
};
int main()
{
// ...
EUI_LINK( comms_interfaces );
// ...
}

Note that the & should not be used with the EUI_LINK argument as it is incompatible with the array-length macro.

Parser Callbacks

The interface_cb member of the eui_interface_t accepts a pointer to a function, and will be called by the packet handler with the following argument (uint8_t):

enum eui_callback_codes {
EUI_CB_GENERIC = 0,
EUI_CB_TRACKED,
EUI_CB_UNTRACKED,
EUI_CB_PARSE_FAIL,
EUI_CB_LAST_ENUM,
};

This information allows a developer to receive a callback during the packet handling process, with some minimal information about the packet being handled.

This is intended for use by advanced users who are comfortable reaching into the eui_interface_t .packet and reading data in order to extend their applications functionality.

It can also be used for error handling.

void eui_parser_callback( uint8_t message )
{
switch(message)
{
case EUI_CB_TRACKED:
// UI received a tracked message ID and has completed processing
break;
case EUI_CB_UNTRACKED:
{
// UI passed in an untracked message ID
// Grab parts of the inbound packet which are are useful
eui_header_t header = serial_interface.packet.header;
uint8_t *name_rx = serial_interface.packet.id_in;
void *payload = serial_interface.packet.data_in;
// See if the inbound packet name matches our intended variable
if( strcmp( (char*)name_rx, "test" ) == 0 )
{
// Ensure the UI is sending a variable of the right type
if( header.type == TYPE_UINT16 )
{
// Call a function or do something with the payload...
userspace_function( arg1, arg2 );
}
}
}
break;
case EUI_CB_PARSE_FAIL:
// Inbound message parsing failed, this callback help while debugging
break;
}
}

Message Objects

The most commonly used type with Electric UI, the eui_message_t contains the information required for outbound packet generation, and forms the backbone of the 'tracked message' helper functionality.

typedef struct {
const char* msgID;
uint8_t type;
uint16_t size;
union {
void *data;
eui_cb_t callback;
} ptr;
} eui_message_t;

Message Identifier

This is typically the string for the data being transported. You can alias these to a byte(s) if you prefer expressing your data as a series of incremental values.

  • 0-length identifiers are illegal at a protocol level.
  • 0x00 is an illegal identifier.
  • It has a maximum length of 16 characters.

Types

The default set of types provided by Electric UI cover most base types for typical platforms. We use the stdint.h notation for numeric types wherever possible.

When default types are used, no additional code is required on the UI side for encoding/decoding the payload.

enum eui_type {
TYPE_CALLBACK = 0,
TYPE_CUSTOM,
TYPE_OFFSET_METADATA,
TYPE_BYTE,
TYPE_CHAR,
TYPE_INT8,
TYPE_UINT8,
TYPE_INT16,
TYPE_UINT16,
TYPE_INT32,
TYPE_UINT32,
TYPE_FLOAT,
TYPE_DOUBLE,
};

EUI_OFFSET_METADATA is an internal/private type used as part of split-packet transfers.

If you aren't using fixed width data types, make sure you are using the types which correspond to the size of your underlying data for your given architecture.

eui_typeMacroSize in Bytes
TYPE_BYTEEUI_BYTE1
TYPE_CHAREUI_CHAR1
TYPE_INT8EUI_INT81
TYPE_UINT8EUI_UINT81
TYPE_INT16EUI_INT162
TYPE_UINT16EUI_UINT162
TYPE_INT32EUI_INT324
TYPE_UINT32EUI_UINT324
TYPE_FLOATEUI_FLOAT4
TYPE_DOUBLEEUI_DOUBLE8

Tracked Messages

The default behaviour of the electricui-embedded library relies on the concept of tracked messages.

This functionality reduces the effort of handling variables which reside on the embedded system and need to be queried or written to by the UI.

In your code, defining an array of eui_message_t is all that's needed, along with passing the pointer to said array during initialisation.

There are a set of macros which simplify declaring tracked variables, as the full data structure can result in a dense declaration.

eui_message_t tracked_variables[] = {
{ ... },
{ ... },
EUI_UINT8_ARRAY( "pwm_motors", pwm_settings ),
EUI_FLOAT_RO( "temp", drive_temperature_C ),
EUI_CUSTOM( "9dof_imu", mpu_9250_data ),
}

If developing a C only project, designated initialisers can reduce the amount of boilerplate involved with manual declaration.

Macros

Message declaration macros help make tracking variables easier, and a bit safer by preventing accidentally leakage of larger arrays. By default, these macros restrict the size of accessible data to that of the underlying type.

  • Append the _RO to enforce read-only behaviour on the tracked variable.
  • Append _ARRAY and the macro will share the full size of the tracked data, allowing automatic array behaviour.
  • Use ARRAY_RO for a read-only array.
EUI_CUSTOM()
EUI_FUNC()
// Default types
EUI_CHAR()
EUI_INT8()
EUI_INT16()
EUI_INT32()
EUI_UINT8()
EUI_UINT16()
EUI_UINT32()
EUI_FLOAT()
EUI_DOUBLE()
// Read only examples
EUI_CUSTOM_RO()
EUI_UINT16_RO()
...
// Array examples
EUI_CHAR_ARRAY()
EUI_FLOAT_ARRAY()
...
// RO Array examples
EUI_UINT8_ARRAY_RO()
EUI_DOUBLE_ARRAY_RO()
...

Due to C's compile-time macro support and underlying data structures, EUI_CUSTOM behaves the same with single or arrays of elements.

EUI_FUNC() is always read-only by design, as external devices can't modify internal function pointers.

Helpers

find_tracked_object returns the pointer to the eui_message_t with corresponding identifier string.

eui_message_t * find_tracked_object( const char * msg_id );

The same pointer could also be found by directly grabbing the relevant eui_interface_t array index like so: &tracked_variables[6].

Setup/Init

void eui_setup_tracked( eui_message_t *msgArray, eui_variable_count_t numObjects );
void eui_setup_identifier( char * uuid, uint8_t bytes );

Receiving Data

When data has been received by the hardware, the firmware developer passes the data to the Electric UI library by calling the eui_parse function with the data, and a pointer to the interface the data 'belongs' to.

eui_errors_t eui_parse( uint8_t inbound_byte, eui_interface_t *p_link );

It returns a single byte which is packed with fields to indicate success/failure in the various parts of processing inbound data and packets.

Sending Data

A callback is provided to Electric UI through the interface structure. This callback has the internal type of

typedef void (*callback_data_out_t)(uint8_t*, uint16_t);

Typically, the firmware developer will have a HAL function which is capable of writing a byte(s) to the relevant communications interface.

In practice, this function would look similar to the following:

void serial_write_cb( uint8_t *data, uint16_t len )
{
hal_uart_write( HAL_USART_3, data, len ); // or equivalent
}

If needed, iterate over the data[] and write bytes out individually, or copy to ring buffers as required by many DMA based peripherals.

Send Functions

Sometimes applications logic needs to immediately inform the UI of some change to data, like during major state changes, or in situations where it's inefficient for the UI to query the microcontroller for constant updates.

Helper functions can provide identifier string lookup, and its also possible to send using a eui_message_t pointer.

In situations where more than one communications link is available, the link which last received a successful message is used. This can be manually overridden by using eui_send_tracked_on() or eui_send_untracked_on() with a pointer to the desired eui_interface_t.

void eui_send_tracked( const char * msg_id );
void eui_send_tracked_on( const char * msg_id, eui_interface_t *interface );
void eui_send_untracked( eui_message_t *msg_obj_ptr );
void eui_send_untracked_on( eui_message_t *msg_obj_ptr, eui_interface_t *interface );

A packet can be sent manually. Specify the output function pointer, pointer to a eui_message_t (generated at run-time even), and some packet settings. Refer to the eui_pkt_settings_t in the eui_types.h file.

uint8_t eui_send( callback_data_out_t output_function,
eui_message_t *msgObjPtr,
eui_pkt_settings_t *settings );
// optionally enabled at compile time
uint8_t eui_send_range( callback_data_out_t output_function,
eui_message_t *msgObjPtr,
eui_pkt_settings_t *settings,
uint16_t base_addr,
uint16_t end_addr );

Error Handling

The byte returned by eui_parse includes densely packed sub-state information. This can be used during debugging to identify where an error occurred.

typedef struct {
unsigned parser : 2;
unsigned untracked : 1;
unsigned action : 2;
unsigned ack : 1;
unsigned query : 2;
unsigned reserved : 1;
} eui_errors_t;
enum eui_parse_errors {
EUI_PARSER_OK = 0,
EUI_PARSER_IDLE,
EUI_PARSER_ERROR,
};
enum eui_action_errors {
EUI_ACTION_OK = 0,
EUI_ACTION_CALLBACK_ERROR,
EUI_ACTION_WRITE_ERROR,
EUI_ACTION_TYPE_MISMATCH_ERROR,
};
enum eui_ack_errors {
EUI_ACK_OK = 0,
EUI_ACK_ERROR,
};
enum eui_query_errors {
EUI_QUERY_OK = 0,
EUI_QUERY_SEND_ERROR,
EUI_QUERY_SEND_OFFSET_ERROR,
};
enum eui_output_errors {
EUI_OUTPUT_OK = 0,
EUI_OUTPUT_ERROR,
};

To use this functionality, hold the returned byte from eui_parse() and then use normal struct dot notation to access the specific error states.

eui_errors_t inbound_error = eui_parse( inbound_byte, &serial_interface);
if( inbound_error )
{
// at least one part of inbound packet handling encountered issues
}
if ( inbound_error.action == EUI_ACTION_TYPE_MISMATCH_ERROR )
{
// the inbound packet's header type doesn't match the internal type
// in this case, no write/callback action would have occured
}

Private Structures & Functions

Some functionality isn't exposed on the public interface, but can be useful while debugging or if modifying the library.

// 24-bit header
typedef struct {
unsigned data_len : 10;
unsigned type : 4;
unsigned internal : 1;
unsigned offset : 1;
unsigned id_len : 4;
unsigned response : 1;
unsigned acknum : 3;
} eui_header_t;
// Shorthand header
typedef struct {
unsigned internal : 1;
unsigned response : 1;
unsigned type : 4;
} eui_pkt_settings_t;

Transport Types

typedef struct {
unsigned state : 4;
unsigned id_bytes_in : 4;
unsigned data_bytes_in : 10;
uint8_t frame_offset;
} eui_parser_state_t;
//inbound packet buffer and state information
typedef struct {
eui_parser_state_t parser;
eui_header_t header;
uint8_t msgid_in[15];
#ifndef EUI_CONF_OFFSETS_DISABLED
uint16_t offset_in;
#endif
uint8_t data_in[PAYLOAD_SIZE_MAX];
uint16_t crc_in;
} eui_packet_t;

Utility Functions

void crc16( uint8_t data, uint16_t *crc );
void
validate_offset_range( uint16_t base,
uint16_t offset,
uint8_t type_bytes,
uint16_t size,
uint16_t *start,
uint16_t *end );

Decoder

enum parseStates {
exp_frame_offset = 0,
exp_header_b1,
exp_header_b2,
exp_header_b3,
exp_message_id,
exp_offset_b1,
exp_offset_b2,
exp_data,
exp_crc_b1,
exp_crc_b2,
};
uint8_t decode_packet( uint8_t inbound_byte, eui_packet_t *p_link_in );
uint8_t parse_decoded_packet( uint8_t byte_in, eui_packet_t *p_link_in );

Encoder

uint8_t encode_header( eui_header_t *header, uint8_t *buffer );
uint8_t encode_framing( uint8_t *buffer, uint16_t size );
uint8_t encode_packet_simple( callback_data_out_t output_function,
eui_pkt_settings_t *settings,
const char *msg_id,
uint16_t payload_len,
void* payload );
uint8_t encode_packet( callback_data_out_t out_char,
eui_header_t *header,
const char *msg_id,
uint16_t offset,
void* payload );