Technical Article

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

August 09, 2023 by Michael Levanduski

Refining and integrating a front-end HMI application to an API middle layer with real-time data display and historian capabilities for short-term data visualization.

The article concludes a series in web-based HMI development.

Previous articles in the series cover the edge computing component, the middle data server layer component, and the first steps in developing a graphical user interface.


 

In the previous article, we left off on a puzzling behavior of the HMI application. HTTP GET requests were successful, yet no data was populated on the front end of our application. To debug, open up the developer tools on the webpage of the HMI using the keyboard shortcut f12. The tools here are useful for debugging code and networking issues in web development. In the console tab, there should be an error resembling the following:

 

Warning error message

Figure 1. Integration error. Image provided by the author

 

Notice that the error is referencing something called a CORS policy.

 

What is Cross-origin Resource Sharing (CORS)?

CORS refers to the scenario where a front-end application has Javascript code running that references a back-end service of a different origin. An origin is defined as the URI consisting of protocol, domain, and port. The error received in the HMI application is a result of our API server and front-end HMI application having different origins:

  • API Server = http://127.0.0.1:8000
  • HMI App = http://127.0.0.1:5500

Even though the two applications run on the local machine’s loopback address (127.0.0.1) using HTTP protocol, they run on two different ports; 8000 and 5500, respectively. Therefore, they are of different origins, and port 5500 needs to be added to the CORS policy in order to share data from the API with the Javascript code running in the HMI application.

 

How to Add the HMI Front-end to CORS

Luckily, FastAPI has extensive documentation and a section dedicated to CORS. To add the HMI application to the CORS policy of the API, we will modify the main.py API code in the gray sections as follows:

from fastapi import FastAPI
from pydantic import BaseModel
import uuid
from fastapi.middleware.cors import CORSMiddleware

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

app = FastAPI()

origins = [
    "http://127.0.0.1:5500"
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials = True,
    allow_methods=["GET"],
    allow_headers=["*"],
)

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

Flipping back to the browser running the HMI application, data is at last being populated:

 

Actively sharing data with HMI

Figure 2. Data is now shared between the API server and the HMI application. Image provided by the author

 

Formatting the HMI With CSS

Note that the format of the HMI application does not look professional. The operators and technicians of this HMI would likely have pointed feedback regarding the UI. Let us format the structure in a more visually appealing manner using CSS. We will create a new file in the root directory called styles.css:

 

styles css in the root directory

Figure 3. Styles.css in the root project directory. Image provided by the author

 

Within that file, we will enter the following:

body {
    background-color: #f2f2f2;
  }


  h1 {
    color: #333;
    text-align: center;
    margin-top: 20px;
  }


  table {
    width: 100%;
    margin: 20px auto;
    background-color: #fff;
    border-collapse: collapse;
    border: 1px solid #ccc;
  }


  table th,
  table td {
    padding: 10px;
    text-align: center;
  }


  table thead {
    background-color: #eee;
  }

The result appears below:

 

Better format for web HMI

Figure 4. CSS applied to the front-end HMI. Image provided by the author

 

Historian Capability

The last step in our initial demo HMI front-end user interface is to show some rough visual of the Tag A value over time. To accomplish this, we can use the highcharts package in Javascript. Please make the following updates to the script.js file below:

// Create an empty array to store the historical data
var historicalData = [];

// 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);
  }

// Function to update the chart with the historicalData
function updateChart() {
  // Extract tagName and tagValue arrays from the historicalData
  var tagNameArray = historicalData.map(data => data.tagName);
  var tagValueArray = historicalData.map(data => data.tagValue);

  // Chart configuration
  var chartOptions = {
    chart: {
      type: 'line',
      renderTo: 'chart'
    },
    title: {
      text: 'Tag Values Over Time'
    },
    xAxis: {
      categories: tagNameArray
    },
    yAxis: {
      title: {
        text: 'Tag Value'
      }
    },
    series: [{
      name: 'Tag Value',
      data: tagValueArray
    }]
  };

  // Create the chart
  var chart = new Highcharts.Chart(chartOptions);
}

// Initial data fetch
fetchData();

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

 

Lastly, we will add some styling to the highchart in the styles.css file. Please make the following changes below:

body {
    background-color: #f2f2f2;
  }


  h1 {
    color: #333;
    text-align: center;
    margin-top: 20px;
  }


  table {
    width: 100%;
    margin: 20px auto;
    background-color: #fff;
    border-collapse: collapse;
    border: 1px solid #ccc;
  }


  table th,
  table td {
    padding: 10px;
    text-align: center;
  }


  table thead {
    background-color: #eee;
  }


  #chart {
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
    background-color: #fff;
    border: 1px solid #ccc;
  }

When all is put together, the HMI should appear similar to below:

 

Web HMI with historian graphFigure 5. Front-end result of a rough HMI end-to-end project. Image provided by the author

 

Conclusion

We hope this article series has given you a better understanding of what is involved in building a custom HMI project. Remember that the skills required are broad, and to build and maintain a solution at an enterprise scale, a lot more work is involved than the simple and rudimentary approach taken in this series. Some further articles will be derived from this initial tutorial covering more advanced topics.