UI Development

Now we have the toolchain installed and a microcontroller running some example code, lets setup and start customising the template in a sandbox environment and control our hardware!

Starting with a template

From the command-line, run: arc init hello-electric to populate the template in a new folder called hello-electric.

You can use arc init to use the working directory if it's empty.

This will bootstrap a new project, follow the prompts to enter the product name.

template-generation-with-arc

Product Name is the human readable name used in the application title-bar, installers/uninstallers, OS applications lists, and in package metadata.

Examples: Fancy Datalogger, Killer Robot UI, Harmonica Test-Jig Controller

This process will download all dependencies required and set everything up.

Launching the development sandbox

To run the program, jump into the top level project directory and run arc start to boot the development interface.

template-startup-terminal-text

You should see the main application window open and show a connection screen, with either a split-window, or separate debugger window.

template-first-run

When the development environment started, a secondary section or window will have also spawned, presenting the debugger for the render thread. This debugger is useful for UI troubleshooting and performance analysis.

We strongly recommend splitting it into a separate window and changing the debugger view to the Console tab.

template-undock

You can also toggle the debugger through the title-bar menu DevTools > Toggle Developer Tools.

Editing the template

Open the arc generated project folder hello-electric with your IDE/text editor of preference. We strongly recommend VS Code, as the real-time Typescript hinting integration works wonderfully.

The main application sources are in /src/application/pages/DevicePages, with OverviewPage.tsx being the first main view. Open it in your editor and you'll see the layout of the page.

Typescript files start with imports at the top, followed by the component definition OverviewPage which contains the UI layout.

// Component imports belong at the top
import React from 'react'
// ...
import { Printer } from '@electricui/components-desktop'
 
// OverviewPage is an exported React function which renders a layout
export const OverviewPage = () => {
// Hook usage goes here
 
return (
// UI Layout goes here
<React.Fragment>
Internal battery level: <Printer accessor="batt" />
</React.Fragment>
)
}

The hello-blink demo firmware exposes LED blinking parameters, allowing you to vary the frequency, enable/disable blinking, and manually override the LED state. We'll build out the UI and firmware to provide better control over the hardware.

Displaying data as text

The template already has some simple components to visualise the hello-blink example. We'll be adding more components to the UI to get started.

We want to print the led_state variable as text, so we'll use a Printercomponent and some human readable text. Electric UI components use an accessor property to provide the data source.

import { Printer } from '@electricui/components-desktop'

We'll import it at the top of our file, then use the <Printer /> component in our layout.

LED state: <Printer accessor="led_state" />

The printer can go anywhere inside the <React.Fragment> ... </React.Fragment> block, but we suggest above the slider (inside the <Card> block).

Save the source file, the UI will update and you should see the text changing as the light on your hardware blinks.

If you play with the slider, you'll notice that the led_state doesn't always keep up with the hardware LED state, and for rapid blinking, the graph shows the wrong shape. You might have heard of Nyquist Theorem before, and the slow polled variable requests made by the template are responsible.

Polling hardware for data

The template includes a IntervalRequester near the top of the body which requests variables every 250 milliseconds.

<IntervalRequester
variables={['led_state', 'led_blink', 'lit_time']}
interval={250}
/>

Change the interval to 50ms, save, and see at how this changes the graphed data.

Faster polling interval makes the graph more accurately represent the blinking light

While its tempting to continue 'increasing the polling frequency', its not the best way to capture rapid changes in state. Likewise, it's inefficient to waste bandwidth polling settings or sensor data more often than the underlying data changes.

Pushing UI updates from hardware

Instead of polling the microcontroller periodically for variable states, we will change the embedded firmware to send updated led_state data as the changes occur.

Open up your microcontroller's editor to make a small tweak to the demo firmware flashed earlier.

Add a eui_send_tracked( "led_state" ); function call to the blink logic, reflash your hardware, and play with the UI.

if( millis() - led_timer >= glow_time )
{
led_state = !led_state; //invert led state
led_timer = millis();
eui_send_tracked( "led_state" ); // send the new value to the UI
}

The UI is now updating as soon as the LED changes state, and the graph should properly represent the blinking LED's behaviour.

Hardware pushed variables are lowest latency and best represent the hardware's blinking light

Lets add a two-state switch to toggle the blinking behaviour on and off. We're going to use a Switch component.

We've already imported the Slider component from that package, so just add the Switch import to the same import.

import { Slider, Switch } from '@electricui/components-desktop-blueprint'

Then, just below our Printer, lets add the switch.

<Switch
unchecked={0}
checked={1}
accessor={state => state.led_blink}
writer={(state, value) => {
state.led_blink = value
}}
>
Toggle LED Blinker
</Switch>

It accepts the unchecked and checked properties which let you specify the intended state of the variable.

For the switch to show the correct position as the hardware changes state, we use the accessor property to specify the input state.

When you click on the switch to toggle the setting, you are writing a new value against the current state. This is done using the writer property, where we update the led_blink state with the value given to us by the component.

Toggling the switch will stop the blinking, but the LED will stick in an ON or OFF state when the mode changes.

The switch lets us stop the blinker at will

Try adding another Switch, or a Checkbox component to manually set the led_state variable to 0 or 1 while led_blink is disabled.

Cleaning up the layout

Some new components have been added, but any reasonable UI will need the ability to group and position components to control their UI layout.

Compositions form the skeleton of the UI, providing rules which format the UI components. The template uses the areas feature to define the arrangement of zones which are used during layout.

const layoutDescription = `
Chart Chart
Light Slider
`
Rectangular grid of boxes (chart chart light slider) representing the layout

Because the Chart area was referenced twice, the layout engine treats them as one larger area. This templated layout is used in the composition by passing in the description to the areas property:

<Composition areas={layoutDescription} gap={10} autoCols="1fr">
{Areas => (
<React.Fragment>
<Areas.Chart> ... </Areas.Chart>
 
<Areas.Light> ... </Areas.Light>
 
<Areas.Slider> ... </Areas.Slider>
</React.Fragment>
)}
</Composition>

We're going to put another Composition inside the <Areas.Slider> area to arrange the Printer and Switch components like so:

Nesting compositions allows rich layout behaviour to be easily declared
<Composition templateCols="1fr 1fr" margin={10}>
<Box>
LED state: <Printer accessor="led_state" />
</Box>
<Box>
<Switch
unchecked={0}
checked={1}
accessor={state => state.led_blink}
writer={(state, value) => {
state.led_blink = value
}}
>
Toggle Blinker
</Switch>
</Box>
</Composition>

The templateCols="1fr 1fr" property tells Composition that we want the layout to use two evenly sized columns. We add some spacing from our parent Card by setting the margin.

After saving, the components will position themselves horizontally.

The nested composition controls the layout of the printer and switch

Try vertically centering the layout by adding a justifyItems="center" property to the Composition. The layouts article teaches the power of composition driven layouts in more detail.

Moving on

With the basics worked out, you can start making a UI for your own project!

We strongly recommend reading the debugging, layout and custom data structures guides as a primer on the most important areas during development.

Theelectricui-embedded/examples/demonstrations folder has a set of self contained demos which expose simulated data for examples like water heaters, garage doors or test equipment. Try building a simple UI against simulated data, or dive into your own project!