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 callbackeui_interface_t serial_interface = EUI_INTERFACE_CB( &serial_write, &eui_callback );
#define HEARTBEAT_EXPECTED_MS 800 // Expect to see a heartbeat within this thresholduint32_t last_heartbeat = 0; // Timestamp in millisecondsuint8_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 theelectricui-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 calledinterval_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 );
- Functions are also available which accept a pointer to the tracked
-
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" );
andinterval_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.tsexport type IntervalSenderRequest = { interval : number id : string}
// Codec goes in transport-manager/config/codecs.tsxexport 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 0x42eui_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 startupvoid 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.