Building a React F# UI for an Embedded System
source link: https://medium.com/@viktorschepik/building-a-react-f-ui-for-an-embedded-system-115f9e89522b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Building a React F# UI for an Embedded System
When the UI has to wait for values
TLDR: We build a React F# single page application that is able to handle values that are sent to and received from an embedded system, considering that the communication takes noticeable time.
Quite often, embedded systems need a user interface but do not have their own display. For that, they can offer a Web user interface like a Single Page Application (SPA) and a corresponding REST API that is used by the SPA.
When you build an SPA for an embedded system, you will realise that there are some challenges that you only seldom encounter in a different application domain. One main issue is the reaction time of an embedded system regarding the user interface. That means, for example, when the user wants to switch on or off something controlled by the embedded system, this could take several seconds. And the user interface has to wait for the confirmation that the “switch” activity has been successful. You could think of showing some waiting state or disabling the UI component in order to avoid that the user desperately repeats the action of clicking. Let’s see how we could accomplish this in a decent way with F#, Fable
Let’s assume the following e-mobility scenario as an example:
We have a charging station at home and want to charge our electric vehicle.
The charging station is connected to a power net and can conduct the electrical power to the vehicle. The vehicle decides on its own if and when to charge the vehicle battery, but it is dependent on the permission of the charging station.
So when we select the “Allow charging” option at the SPA, the corresponding command is transferred to the charging station. Then the charging station can switch the electrical connection and communicate to the vehicle that it is allowed to charge. It could take some time until the electrical connection has been established and the communication with the vehicle has finished. Only then the charging station can inform the SPA that everything has worked out. Plenty of things could go wrong when dealing with embedded systems because they operate in the physical world where mechanical interactions, temperature conditions and so on directly affect the behaviour of the system.
But how do we represent this timing-related behaviour appropriately in an SPA? As you all probably have figured out, we can define a nice solution in F#. For that, let’s concentrate on the user interaction where the user selects one of the two following options:
- Allow charging
- Disallow charging
and represent it as an F# discriminated union:
type ChargingPermission =
| Allow
| Disallow
At the UI we represent it as options of a radio group:
Waiting when receiving values
When the WebBrowser loads the SPA, the charging permission state is unknown until the current value has been requested at the API of the charging station and until a response has been received.
Now it is time to represent the Receive state of the charging permission. A receive value can be in the state of
- being available and having a domain value
- being unavailable and the domain value is unknown
- receive has failed and error information is provided.
Again, an F# discriminated union will solve the problem by representing three states:
type ReceiveValue<'T> =
| Available of 'T
| Unavailable
| ReceiveFailed of ErrorInfo
In order to represent the received value of the charging permission, we get
let (chargingPermission: ReceiveValue<ChargingPermission>) = Unavailable
and this represents the initial state when the SPA is loaded. A UI component built with F#, the Fable compiler, Feliz and TailwindCSS could look like this:
[<ReactComponent>]
static member RadioInputReceiveOnly
(inputValue: ChargingPermission)
(chargingPermission: ReceiveValue<ChargingPermission>)
=
let inputValueStr = $"%A{inputValue}"
Html.div [
prop.classes [ "flex"; "gap-2" ]
prop.children [
Html.input [
prop.type'.radio
prop.name "charging-permission"
prop.value inputValueStr
prop.isChecked (
match chargingPermission with
| Available permission when permission = inputValue -> true
| Available _
| Unavailable
| ReceiveFailed _ -> false
)
prop.disabled (
match chargingPermission with
| Available _ -> false
| Unavailable
| ReceiveFailed _ -> true
)
]
Html.label [ prop.for' inputValueStr; prop.text inputValueStr ]
]
]
[<ReactComponent>]
static member ChargingPermissionComponent() =
let receivedChargingPermission, setReceivedChargingPermission =
React.useState Unavailable
React.useEffectOnce (fun () -> RestApi.getChargingPermission setReceivedChargingPermission)
Html.div [
prop.children [
Html.div [ prop.classes [ "text-xl"; "my-4" ]; prop.text "Charging Permission" ]
ChargingPermissionComponent.RadioInputReceiveOnly Allow receivedChargingPermission
ChargingPermissionComponent.RadioInputReceiveOnly Disallow receivedChargingPermission
]
]
Let me guide you through the main sequences by depicting it in the following state diagrams. On the left side, the receive state of the charging permission is shown in the context of the main UI component. On the right side, you can see the asynchronous communication with the REST API that triggers the state change on the left side.
The UI component RadioInputReceiveOnly
handles at this stage of development the receive state only and renders the radio buttons depending on the receive state in the match chargingPermission with
sections.
You could enhance those sections with some error handling, too, which is omitted here. At this stage, you probably got the idea of how to handle the different receive states.
What about changing a value? This gets indeed interesting and can be solved decently with F# language constructs.
Waiting when sending values
In the upper code sample, the radio button selection can’t be changed. So let’s enhance the example in order to be able
- to change the value,
- send the selected value to the REST API,
- wait for approval by the REST API and
- change the radio buttons’ state accordingly.
Sending values can be represented by several states:
type ErrorInfo =
| Unauthorized
| InternalServerError
type SendValue<'T> =
| Uninitialized
| Initial of 'T
| Sending of 'T
| Sent of 'T
| SendFailed of 'T * ErrorInfo
Mapping the receiving and sending states to the UI component states could look like the state diagrams below.
In the diagram, the corresponding states are horizontally aligned. The grey radio buttons in the middle column shall represent the disabled state. You could choose to represent the waiting states differently, but for simplicity, we use the disabled state of the HTML input element.
You could represent the sending state as a React state:
let sendChargingPermission, setSendChargingPermission = React.useState Uninitialized
When the user selects on of the two options, Allow or Disallow, the SPA sets this state to Sending
and sends the value to the REST API. When the SPA obtains the response it sets the state to Sent
if everything went well and otherwise to SendFailed
when something went wrong.
The UI component has now all information to switch the radio button states. By using match
clauses we can express literally what should happen at the UI. For example to define the disabled
state of a radio button we match
like this:
prop.disabled (
match props.SendChargingPermission with
| Initial _
| Sent _
| SendFailed _ -> false
| Uninitialized
| Sending _ -> true
)
or a little bit more sophisticated the checked
state like this:
prop.isChecked (
match props.SendChargingPermission, props.ReceiveChargingPermission with
| (Initial permission | Sent permission), _ when permission = props.RadioInputValue -> true
| (Sending _ | SendFailed _), Available permission when permission = props.RadioInputValue ->
true
| _ -> false
)
This code serves just to give an idea and should better be read in the full code context below.
Summary
The decent aspect of representing remote value states by F# discriminated unions is that you can represent all asynchronous events and states in your source code and handle it in a nice way using the match
construct of F#.
The full solution
type ChargingPermission =
| Allow
| Disallow
type ErrorInfo =
| Unauthorized
| InternalServerError
type ReceiveValue<'T> =
| Available of 'T
| Unavailable
| ReceiveFailed of ErrorInfo
type SendValue<'T> =
| Uninitialized
| Initial of 'T
| Sending of 'T
| Sent of 'T
| SendFailed of 'T * ErrorInfo
type ChargingPermissionComponent =
[<ReactComponent>]
static member RadioInput
(
radioInputValue: ChargingPermission,
receiveChargingPermission: ReceiveValue<ChargingPermission>,
sendChargingPermission: SendValue<ChargingPermission>,
onChange: ChargingPermission -> unit
) =
let inputValueStr = $"%A{radioInputValue}"
Html.div [
prop.classes [ "flex"; "gap-2" ]
prop.children [
Html.input [
prop.type'.radio
prop.name "charging-permission"
prop.value inputValueStr
prop.isChecked (
match sendChargingPermission, receiveChargingPermission with
| (Initial permission | Sent permission), _ when permission = radioInputValue -> true
| (Sending _ | SendFailed _), Available permission when permission = radioInputValue -> true
| _ -> false
)
prop.disabled (
match sendChargingPermission with
| Initial _
| Sent _
| SendFailed _ -> false
| Uninitialized
| Sending _ -> true
)
prop.onChange (fun (selectedValue: string) ->
if selectedValue = $"%A{Allow}" then
onChange Allow
elif selectedValue = $"%A{Disallow}" then
onChange Disallow
else
())
]
Html.label [ prop.for' inputValueStr; prop.text inputValueStr ]
]
]
[<ReactComponent>]
static member ChargingPermissionComponent() =
let receiveChargingPermission, setReceiveChargingPermission =
React.useState Unavailable
let sendChargingPermission, setSendChargingPermission = React.useState Uninitialized
React.useEffectOnce (fun () ->
RestApi.getChargingPermission (fun receiveChargingPermission ->
setReceiveChargingPermission receiveChargingPermission
match receiveChargingPermission with
| Available chargingPermission -> setSendChargingPermission (Initial chargingPermission)
| Unavailable
| ReceiveFailed _ -> ()))
Html.div [
prop.children [
Html.div [ prop.classes [ "text-xl"; "my-4" ]; prop.text "Charging Permission" ]
ChargingPermissionComponent.RadioInput(
radioInputValue = Allow,
receiveChargingPermission = receiveChargingPermission,
sendChargingPermission = sendChargingPermission,
onChange =
(fun value ->
setSendChargingPermission (Sending value)
RestApi.putChargingPermission value setSendChargingPermission)
)
ChargingPermissionComponent.RadioInput(
radioInputValue = Disallow,
receiveChargingPermission = receiveChargingPermission,
sendChargingPermission = sendChargingPermission,
onChange =
(fun value ->
setSendChargingPermission (Sending value)
RestApi.putChargingPermission value setSendChargingPermission)
)
match sendChargingPermission with
| SendFailed(_, errorInfo) -> Html.div [ prop.text $"Error: %A{errorInfo}" ]
| _ -> ()
]
]
Clone this sample project to run it locally and play with it.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK