Extending embedded functionality

The electricui-embedded library and protocol are intentionally simplistic to better support smaller hardware targets. With some minimal additions in your firmware, more complex behaviours can be integrated.

Determine connection status with Heartbeats

An internal message h is used by the UI for acknowledged heartbeats against the embedded hardware. These heartbeats are used to determine link latency, reliability and to detect dropped connections.

By setting up an interface callback on the hardware side, we can determine when heartbeat packets are received.

You'll need to replace the HAL_get_ms() with your platform's equivelant timing function, i.e. millis() with Arduino.

// Use the EUI_INTERFACE_CB macro to include the diagnostics callback
eui_interface_t serial_interface = EUI_INTERFACE_CB( &serial_write, &eui_callback );
#define HEARTBEAT_EXPECTED_MS 800 // Expect to see a heartbeat within this threshold
uint32_t last_heartbeat = 0; // Timestamp in milliseconds
uint8_t heartbeat_ok_count = 0; // Running count of successful heartbeat messages
void eui_callback( uint8_t message )
{
if( message == EUI_CB_TRACKED )
{
// We reach into the interface structure to grab the ID
uint8_t *name_rx = serial_interface.packet.id_in;
// Check if the internal heartbeat message came in
if( strcmp( (char*)name_rx, EUI_INTERNAL_HEARTBEAT ) == 0 )
{
// If the previous heartbeat was within the threshold, increment the counter
if( HAL_get_ms() - last_heartbeat < HEARTBEAT_EXPECTED_MS )
{
heartbeat_ok_count++;
}
// Take the timestamp of this inbound heartbeat
last_heartbeat = HAL_get_ms();
}
}
// Optionally handle EUI_CB_UNTRACKED and EUI_CB_PARSE_FAIL
}

You can use this information to drive status LEDs based on the heartbeat:

void main_loop( void )
{
// Tasks, handle serial data, etc...
// Calculate the age of the last heartbeat
uint32_t heartbeat_dt = HAL_get_ms() - last_heartbeat;
if( heartbeat_dt < 256 )
{
// Fade out - output PWM is reduced as the heartbeat gets older
uint8_t brightness = 255 - heartbeat_dt;
HAL_set_pwm( LED_PIN, brightness );
}
}

Or detecting if an interface is actively communicating with the UI:

bool is_connection_ok( void )
{
// Check if the most recent heartbeat has timed out
if( HAL_get_ms() - last_heartbeat > HEARTBEAT_EXPECTED_MS )
{
heartbeat_ok_count = 0;
}
// Connection is considered OK if more than 3 heartbeats have arrived within
// the threshold duration
return ( heartbeat_ok_count > 3 );
}

This style of heartbeat timing and LED animation is implemented in the status-error-callbacks example Arduino sketch.

Publishing data to the UI automatically

For graphs or other visual outputs, its often required to stream data to the UI at a fixed rate.

The UI provides a IntervalRequester component which polls the microcontroller for data at specified rates.

This functionality can be improved on platforms with spare resources; the UI specifies a reporting rate and variable(s), and the microcontroller takes responsibiliy for sending them without being polled.

This code is longer and somewhat reusuable, so it's available as a library on Github.

Basic Usage

  • Download the interval_send library alongside the electricui-embedded library, and #include "interval_send.h" in your firmware.

  • Declare the storage pool for the library, and give the library a pointer to that pool with it's size.

    interval_send_requested_t send_pool[5] = { 0 };
    // At runtime, before any other interval_send functions are called
    interval_send_init( &iv_send_pool, 5 );
  • Manually add a sender by calling interval_send_add_id( "led_state", 50 ); with the duration in milliseconds. When added, it will automatically 'start' sending.

    • Functions are also available which accept a pointer to the tracked eui_message_t entry: interval_send_add( &tracked_variables[1], 50 );
  • Call interval_send_tick( HAL_get_ms() ); with the current time in milliseconds whereever convenient, or as often as possible.

    This runs a check of the passed timestamp against the list of sendable messages and their last sent timestamps.

  • Pause or resume the auto-sender (without removing it) using interval_send_stop_id( "id" ); and interval_send_start_id( "id" );.

  • Update the send duration of an existing id by calling interval_send_add_id again with the new timestamp.

  • Remove it from the list entirely with interval_send_remove_id().

Heartbeat based pause/resume

Building on the heartbead demo from earlier, we can stop the microcontroller from attempting to send data to the UI if we haven't seen a few heartbeats in a row.

This can be useful if the serial link is being interrupted by noise, is over a weak wireless link, or just to minimise output data if no UI is connected.

bool hb_ok = ( heartbeat_ok_count > 3 );
interval_send_enable( hb_ok );

Control the library wide send toggle by passing a boolean to interval_send_enable( bool enabled ).

This behaviour is demonstrated in Arduino example: interval_send/examples/heartbeat_pause .

Integration with IntervalRequester

We can add some simple handers to let the UI control interval_send directly.

Configured correctly, the UI side IntervalRequester knows not to poll against hardware and will instead request interval_send behaviour, setting up and tearing down senders as the user interacts with components which require streaming data.

To send requests to the embedded side, the UI uses a custom-encoded packet which includes the interval and message ID. The interval_send library has a pre-made type for this, interval_send_requested_t which contains a uint32_t for the interval and a char buffer for the ID.

Declare an instance of the a tracked custom type and track it as a custom type, we'll call it "isreq".

interval_send_requested_t ui_interval_request = { 0 };
eui_message_t tracked_variables[] =
{
EUI_CUSTOM( "isreq", ui_interval_request ),
// ...
};

In the electricui-embedded interface callback, catch inbound tracked "isreq" packets and call the relevant interval_send library function:

void eui_callback( uint8_t message )
{
if( message == EUI_CB_TRACKED )
{
uint8_t *name_rx = serial_interface.packet.id_in;
// UI sent a request packet
if( strcmp( (char*)name_rx, "isreq" ) == 0 )
{
// Configure the request
// use the data from our tracked variable
interval_send_add_id( ui_interval_request.id, ui_interval_request.interval );
}
}
}

That's it for the embedded side. A complete example can be found in the Arduino example: interval_send/examples/automatic.

The UI setup is pretty straightforward.

This is done by hand right now, but might be incorporated into Electric UI in a later release

// The type goes in application/typedState.ts
export type IntervalSenderRequest = {
interval: number
id: string
}
// Codec goes in transport-manager/config/codecs.tsx
export class IntervalSendSetupCodec extends Codec<IntervalSenderRequest> {
filter(message: Message): boolean {
return message.messageID === 'isreq'
}
 
encode(payload: IntervalSenderRequest) {
const packet = new SmartBuffer({ size: 20 })
 
packet.writeUInt32LE(payload.interval)
 
// Check if the proposed ID is a valid length
if (payload.id.length < 16) {
packet.writeStringNT(payload.id)
} else {
throw new Error('Requested ID is too long')
}
 
return packet.toBuffer()
}
 
decode(payload: Buffer): IntervalSenderRequest {
const reader = SmartBuffer.fromBuffer(payload)
 
const intervalSenderSettings: IntervalSenderRequest = {
interval: reader.readUInt32LE(),
id: reader.readStringNT(),
}
 
return intervalSenderSettings
}
}

With the codec setup, we create our own IntervalPushRequester component, which will have the same API surface as the IntervalRequester .

// TODO link to example repo?

Using the tracked variable array for persistent data

The Electric UI tracked variable model uses the array of eui_message_t metadata as part of it's normal operation. This data provides type, size and naming information with a pointer to the data, which can make a 'save to EEPROM' function pretty easy to write.

#define EEPROM_BASE_ADDR 0x00
#define EEPROM_MAGIC_WORD 0x42
eui_message_t tracked_data[] =
{
EUI_UINT8( "led_blink", blink_enable ),
EUI_UINT8( "led_state", led_state ),
EUI_UINT16( "lit_time", glow_time ),
EUI_UINT16( "uuid", unique_device_id ),
EUI_CHAR_ARRAY("name", device_name ),
// Add callbacks the UI can invoke
EUI_FUNC("save", save_settings ),
EUI_FUNC("clear", erase_settings ),
};
void save_settings( void )
{
uint16_t eeprom_address = EEPROM_BASE_ADDR;
// Write a magic word at the base address which indicates data is written
EEPROM_write(eeprom_address, 0x42 );
eeprom_address++;
// Write payloads from each of the tracked objects in order
for( uint8_t i = 0; i < EUI_ARR_ELEM(tracked_data); i++ )
{
// Only save mutable variables, ignore immutable (read-only) items
if( tracked_data[i].type != TYPE_CALLBACK
&& tracked_data[i].type >> 7 != READ_ONLY_FLAG )
{
// Get the pointer to the source of data we need to save
uint8_t *variable_ptr = (uint8_t*)tracked_data[i].ptr.data;
// Commit the payload to EEPROM byte-by-byte
for( uint16_t j = 0; j < tracked_data[i].size; j++)
{
EEPROM_write(eeprom_address, variable_ptr[j] );
eeprom_address++;
}
}
}
}
// Called during startup
void retrieve_settings( void )
{
uint16_t eeprom_address = EEPROM_BASE_ADDR;
// Only attempt reading if the preamble byte is stored by a previous write
if( EEPROM_read( eeprom_address ) == EEPROM_MAGIC_WORD )
{
eeprom_address++;
for( uint8_t i = 0; i < EUI_ARR_ELEM(tracked_data); i++ )
{
// Don't try and fetch immutable data we didn't save
if( tracked_data[i].type != TYPE_CALLBACK
&& tracked_data[i].type >> 7 != READ_ONLY_FLAG )
{
// Get the pointer to the destination from our tracked variable array
uint8_t *variable_ptr = (uint8_t*)tracked_data[i].ptr.data;
// Read the data out of EEPROM byte-by-byte
for( uint16_t j = 0; j < tracked_data[i].size; j++)
{
variable_ptr[j] = EEPROM_read( eeprom_address );
eeprom_address++;
}
}
}
} // Optionally pass an error if the magic word didn't exist
}

Because this method blindly follows the tracked_data ordering, if the tracked variable array objects are changed, existing EEPROM data should be cleared.

A more reliable, but less space-efficient method might consider copying the message ID strings into EEPROM and performing string based lookups on save and load, along with size data.

This simple implementation is available in the persistence-eeprom example Arduino sketch:

WiFi Connections Setup UX

For devices which need to access a WiFi access point, it can be helpful to connect over USB/Bluetooth or WiFi in Adhoc mode, and browse a list of visible access points.

Device Bridging & Encapsulation

Some systems will use a communications method which isn't directly available to the user's computer. In this case, a bridging device like a RS485 adaptor or a sensor gateway is common. In situations with mesh networks, one bridge might provide access to hundreds devices.

This example adds of this functionality to our electricui-embedded protocol, but we strongly recommend using a protocol specifically designed for mesh use.