How to Design and Deploy a Web-Based HMI Project - Part 3
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:
- 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.
- CSS - Cascading Style Sheets are what make a website look more visually appealing with colors, fonts, layouts, and spacing.
- 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.
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:
- Creating an HTTP GET path or resource in the API server
- Creating a data store for Tag A data
- 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:
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:
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:
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:
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:
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.