How to Design and Deploy a Web-Based HMI Project - Part 4
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:
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:
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:
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:
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:
Figure 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.