Best Practice Tips

Future-proofing

Firmware Version Numbering

From the first version of your firmware, its a good idea to include a tracked variable/field which provides information about your firmware version. This value can be used during troubleshooting, logging, or as part of automated firmware updates (or prompting for updates).

We'd recommend versioning firmware against the SemVer versioning standard. Implementation variations include a simple string like 1.4.2, as an array of uint8_t, or as a 4-byte wide integer formatted in hex like 0x010402, these lowest 3-bytes can shifted out, whilst still remaining human readable.

Hardware Variant Identification

For the same reasons as firmware versions, distinct hardware versions should be accessible, allowing the UI to modify layouts or functionality if hardware isn't available on a particular variant.

Product Identification

If you have room on the microcontroller to spare, consider including any/all of the following fields:

  • Git build tag, build date (example snippets to embed build-time git information for windows or nix compatible buildchains)
  • String which identifies the product variant as a whole (useful when displaying potential hardware in the connections page).
  • UUID for each specific product, useful for implementing UI based support submissions, logging, or even DRM with server-side authenticity checks.

Bootloaders

If possible, flashing boards with a bootloader will allow for a better in-field firmware update or recovery process. While some microcontrollers have inbuilt bootloaders in ROM, and it's possible to flash these from a Electric UI application, an Electric UI aware bootloader will provide you with:

  • Better detection and handling of a device in bootloader mode,
  • Support for progress bars during flash erase, program download, verification stages,
  • Protection against mismatched firmware versions against hardware variants (if implemented properly)
  • Possibility to become platform agnostic by removing reliance on vendor ROM-burned DFU modes.

Data Management

The ultimate goal of a UI is to display or log data with as little delay as possible. Electric UI scales well with masses of variables and components, but the communications between the UI and the microcontroller will often be a bottleneck.

A large part of improving perceived UI performance is reducing the number of packets bouncing between the UI and the microcontroller. The best way to do this is reduce overheads caused by lazy data management.

Configuration settings

When exposing user configurable settings its a good idea to group like variables for a given subsystem.

Where possible, bit-fields or custom sized structure members can reduce overheads while logically grouping the settings into one variable. If you take this naive example which uses discrete integers;

int alert_enabled; // Enable/Disable status alerts
int alert_volume; // Amplifier gain offset [0, 1, 2, 4, MAX ]
int alert_severity_threshold; // Alert severity threshold (WARNING=1, CRITICAL=3)
int alert_allow_user_dismissal; // User can to dismiss alarms

Each of these variables would need RAM and are sent over the communications link individually, used for a small number of possible values out of their capacity. For a 32-bit microcontroller, it would use 16 bytes of RAM, and needs ~44 bytes of data transfer to the UI.

The use of a structure allows same data storage, but improves readability by grouping members under their 'parent' variable, and improves data density as each variable uses the number of bits needed.

typedef struct {
unsigned enable : 1; // Enable/Disable status alerts
unsigned volume : 3; // Amplifier gain offset [0, 1, 2, 4, MAX ]
unsigned severity : 3; // Alert severity threshold (WARNING=1, CRITICAL=3)
unsigned can_cancel : 1; // User can to dismiss alarms
} AlertConfig_t;
AlertConfig_t health_warning_config;
// When using the settings in applications code
if( health_warning_config.enable ) {
play_alert( "default_bell.wav", health_warning_config.volume );
}

Now, the health_warning_config variable takes 1 byte of RAM, and sends 8 bytes to the UI.

Sensor Readings

The key function of most hardware products is sampling a sensor(s), performing some computation, and then triggering IO and displaying the data or logging to a file.

Send multiple sensors in the same packet

Some of the easiest optimisations can be made when many sensors are sampling at similar rates, simply by transferring multiple sensor streams in the same packet. This can be as simple as using an array, or structure for the sensor readings.

In situations where the required data rate for a given variable is vastly different from another, it might still be more efficient to send both variables in the same packet.

Reduce the overheads of communicated data

Consider the logical size of the data being sent to the UI. Quite often, the data type in use is far larger than needed.

A general purpose temperature sensor reading might only need a int8 rather than a float if the extra precision is being truncated on the user's display to 35°C.

Similarly, that same temperature reading could still contain decimal point precision 35.08°C by sending int16 sized data in centi-degrees and reversing the conversion on the UI side temp/10 in the type-transform pipeline.

Minimise overheads with UI side calculations

Further bandwidth reductions can be made by offloading non-essential processing to the machine running the UI.

Instead of sending min/max/average data to the UI for graphing, send the raw or filtered value, and do the computations on the UI side.

We've demonstrated runtime calculations of differentials/integrals here.

Isolation of responsibility

Electric UI's tracked variable system is great for easy setup and managing wide sets of data, but inexperienced users might fall into the trap of making data available globally to add to the tracked variable array.

Widespread use of global variables is a code smell, and managing Electric UI's setup and management alongside business logic can add unwanted clutter.

We recommend creating a new block of code/class responsible for Electric UI and 'configuration' related functionality. This would typically be responsible for:

  • Electric UI's setup and tracked variables,
  • Managing variables which need storage/retrieval from non-volatile storage,
  • Adding bounds checks or conversions to data for safety or human readability.
  • Adding support for 'default value restoration'

Expose the data structures through public functions which set and get variables, and if possible, keep the Electric UI communications calls (read/write to UART etc) closer to the code which manages the underlying communications peripherals.

This style of usage is in the ZaphodBot Delta Robot project

By moving configuration related data to a centralised place, its far easier to add functionality like save/load, reset to defaults, etc.

Compression

In situations where massive data structures appear, or in constrained bandwidth applications, consider the use of on-the-fly compression.

If using Electric UI's tracked variable concept, expose the buffer of data which is already compressed. For a solution that doesn't needlessly compress data and leave it in a buffer, consider catching an inbound request for the data and compressing it just-in-time.

Encryption

We haven't made an example of this yet. If encrypted communications are important to you, contact us and we'll prioritise these examples and docs.