Technical Article

How to Design and Deploy a Web-Based HMI Project - Part 3

August 09, 2023 by Michael Levanduski

Learn the first steps in focusing on the integration of a front-end HMI application to an API middle layer.

This article continues a series about web-based HMI development.

The previous articles in this series cover the edge computing component, and the middle data server layer component.


 

What is a Front-end Application Layer?

The front-end application layer in software development refers to the user-facing side of the application. The user interface is a key facet of the front end, allowing the user to visually see the structure of components and features as well as interact with these components. The front end also serves to present data from back-end services, such as the API server created in Part 2. Some of the main technologies used in the traditional technology stack include:

  1. HTML - Hyper Text Markup Language is a tag-based language that outlines the structure of a web page and where components should appear. The latest version is HTML5 which offers enhancements such as the canvas element for dynamic visuals. HTML5 is the latest incarnation of core HTML.
  2. CSS - Cascading Style Sheets are what make a website look more visually appealing with colors, fonts, layouts, and spacing.
  3. Javascript - Javascript handles any dynamic content within the webpage. In this example, Javascript will be used to generate a dynamic table of Tag A values within the webpage HMI.

 

Front end application

Figure 1. The front-end layer for the HMI application. Image provided by the author

 

Integration to the Middle Layer API

There are a few steps and some code refactoring that we must complete in order to integrate the front-end HMI into the middle-layer API. This includes:

  1. Creating an HTTP GET path or resource in the API server
  2. Creating a data store for Tag A data
  3. Calling the GET path from the front-end application

 

GET Resource and Data Store

Moving back into the main.py file that houses the code that our API server is running, we will need to add a GET route to handle GET requests sent from the HMI application. The GET request can best be described as a pull or fetch request of data from the API server. Note how this differs from the device.py POST request, where data was pushed or forwarded to the API server. We can start by adding a GET route after the POST route within the main.py file:

@app.post("/tags")
async def createTags(tag: Tag):
    dataEntry = {
        'tagName': tag.tagName,
        'tagValue': tag.value
    }
    return f'Successfully received entry: {dataEntry}'

@app.get("/tags/latest")
async def getLatest():
    pass

 

This route will GET the latest tag payload published to the API server upon our front-end application’s request. There are a few approaches to accessing the data sent to the POST route. One method could be to define the dataEntry variable as a global variable that could be accessed by other functions within main.py. However, this is not a scaleable approach, so a preferred method would be to create a data store.

Defining a database would be the correct production approach to take. However, databases are complicated to set up. To keep things simple, we will use a Python list as a data store in the memory of the API application. This is not a recommended production solution since the data store is not persistent. This means that if the API server is reloaded or encounters a fault and is restarted, the data is wiped and starts fresh upon reboot. However, using a list quickly demonstrates the concept for this tutorial:

from fastapi import FastAPI
from pydantic import BaseModel
import uuid

class Tag(BaseModel):
    tagName: str
    value: int

app = FastAPI()

pseudoDatabase = []

@app.post("/tags")
async def createTags(tag: Tag):
    id = str(uuid.uuid4())
    dataEntry = {
        'uuid': id,
        'tagName': tag.tagName,
        'tagValue': tag.value
    }
    pseudoDatabase.append(dataEntry)
    return f'Successfully received entry: {dataEntry}'

@app.get("/tags/latest")
async def getLatest():
    tag = pseudoDatabase[len(pseudoDatabase)-1]
    return tag

 

The changes to our main.py script are highlighted in gray. We added:

  • A GET path that returns the latest Tag A entry in the pseudoDatabase list
  • Added a unique identifier to each of the POST payloads using the uuid library

 

Front-end Application

Finally, we can start building out our front-end framework! In full disclosure, I am not a front-end developer. Much of the HTML, Javascript, and CSS code here was modified from ChatGPT and Stackoverflow to give the audience an idea of what an end-to-end solution could look like. Front-end web development is important, but as we have seen, there is also a great deal of significance that should be placed on data acquisition, API layer, and data storage components of a project of this scope.

To start, we will define an HTML page that we call “index.html” in our directory:

 

Front end file

Figure 2. The index.html file shown in the root directory. Image provided by the author

 

Next, we will populate this HTML file with the HTML tags that define the structure of the page. We will also define a script element, denoted by tags, that utilizes Javascript to send HTTP GET requests to the GET resource we have defined on the API server:

HTML code for displaying data

 

Notice that we are referencing a “script.js” file that will contain the Javascript code. As such, we will need to create the file in the root directory of the project:

 

Front end script file

Figure 3. The script.js file is shown in the root directory. Image provided by the author

 

Within that file, we will add the following code to send HTTP GET requests to the API server and render data for the table object in the index.html file:

// Function to fetch data from the API
function fetchData() {
  fetch('http://127.0.0.1:8000/tags/latest')
    .then(response => response.json())
    .then(data => {
      // Extract tagName and tagValue from the API response
      var tagName = data.tagName;
      var tagValue = data.tagValue;

      // Append the new data to the historicalData array
      historicalData.push({ tagName, tagValue });

      // Update the table
      updateTable(tagName, tagValue);

      // Update the chart
      updateChart();
    })
    .catch(error => {
      console.error('Error:', error);
    });
}

// Function to update the table with the latest data
function updateTable(tagName, tagValue) {
    var table = document.querySelector('table');
    var tbody = table.querySelector('tbody');


    // Remove existing rows
    tbody.innerHTML = '';


    // Create a new row
    var newRow = document.createElement('tr');


    // Create table cells for the tag name and tag value
    var tagNameCell = document.createElement('td');
    tagNameCell.textContent = tagName;


    var tagValueCell = document.createElement('td');
    tagValueCell.textContent = tagValue;


    // Append the cells to the new row
    newRow.appendChild(tagNameCell);
    newRow.appendChild(tagValueCell);


    // Append the new row to the table body
    tbody.appendChild(newRow);
  }

// Initial data fetch
fetchData();

// Refresh data every 5 seconds
setInterval(fetchData, 5000);

 

Going back to the index.html file and opening the web page with the Live Server extension in VS Code, we see the following:

 

Demo HMI rendered view

Figure 4. The initial HMI HTML page results. Image provided by the author

 

Well, that doesn’t look right. Where is the table data?! Let’s check our main.py API server logs for some more information:

 

GET/POST status codes

Figure 5. Status code from GET request. Image provided by the author

 

Now this really doesn’t make sense! How is our API server indicating that GET requests have been successfully sent to our HMI application, yet no data is populating the dynamic table in the application?

In the final article, Part 4 of this series, the root cause of the problem is identified, a solution is presented, and the front-end HMI is refined with features and styles to make it more appealing to end users.