Technical Article

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

August 09, 2023 by Michael Levanduski

The middle layer, or API layer of a custom-built HMI project involves a server that can submit or receive data to and from a device, either to provide the user interface or interact with the machine.

This project continues a series in web-based HMI development, the beginning of which can be found in this previous article.


 

What is a Middle Layer?

A middle or intermediate layer in software development is equivalent to a middleman or broker in other disciplines. In our example, it serves as an intermediatory agent that facilitates the transfer of data between the device and the user-facing HMI front-end application.

At first glance, adding a middle layer may appear to be unnecessary and overly complex. However, the decoupling of responsibility in software development is a critical concept to grasp. It serves to create a modular and scalable solution where engineers can rapidly make changes and enhancements to the architecture to accommodate dynamic business requirements or organizational changes.

In practice, these middle layers are typically implemented as an API or application programming interface running on a connected server device, which may be as simple as a small single-board computer, or a large industrial server. In our HMI architecture, the middle layer resembles below:

 

Middle layer for HMI development

Figure 1. The middle layer for the HMI application. Image provided by the author

 

What is an API?

An API acts as the aforementioned middleman in many software application scenarios. It ensures that data is transferred between two parties in a defined and structured manner. Most modern APIs utilize the HTTP protocol as the means to transfer data. In this paradigm, customers of the API can send an HTTP request to a listening server hosting the API application. The API structure is generally composed of three critical features:

  1. Definition and documentation - Explaining how customers can interact with the API is critical. The two main documentation tools are OpenAPI and RAML.
  2. Code - There needs to be code that defines the API application. There are open-source frameworks that make such a task easier. Some examples include FastAPI and express.js
  3. Server or runtime - The server or runtime environment is the infrastructure that continuously runs the API code that defines the application. It allows customers to access and consume the API, albeit with a few security considerations that we will dive into in more advanced sections.

 

API Code

Programming interfaces can be a difficult concept to grasp without seeing an actual implementation, which is why we will continue where we left off in Part 1. At this time, our device is ready to send HTTP POST requests to an API server, but we do not have an API definition, code, or server defined. To solve this problem, we will utilize Python’s FastAPI library due to the extensive documentation and ease of use of this framework.

Within the same project directory or folder location within the file explorer of your PC, create a new file named “main.py”. Using an IDE such as VS code will make this process seamless. A virtual environment (venv) is a best practice but is not required, and out of scope for this article.

 

Project file directory

Figure 2. File directory. Image provided by the author

 

Within the command line, a pip install of FastAPI is required. The following command will install all necessary dependencies simplifying the process:

pip install fastapi[all]

 

Once installed, we can begin by importing the necessary libraries in the main.py file:

from fastapi import FastAPI
from pydantic import BaseModel

 

Next, we will define an instance of the FastAPI class assigned to a variable named “app”. A class is a key concept of object-oriented programming. However, for the moment understand that this single line of code allows us access to a toolbox of tools that we don’t need to create ourselves:

app = FastAPI()

 

We will need to instruct this application on the type and structure of the data we will be sending to it from our edge device. If you recall, the payload we constructed in part 1 used a Python dictionary containing a tagName and value below:

    # Construct a JSON payload body for Tag A
    payload = {"tagName": tag,
               "value": tagValue
    }

 

The structure and data types within our main.py API application can be modeled using a class. It inherits from the BaseModel class of the Pydantic library which was installed as a dependency of FastAPI with the “[all]” extra requirement specifier in the pip install step. This class will generalize the data types and structure of the payload so that it could be extended to other tags (i.e. Tag B) and not just Tag A:

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

 

Notice that instead of Tag A’s name and value for the keys, we have replaced them with the data type that our API should expect for any tag being passed. This prevents data types from being passed to our API that we have no means of processing (i.e. an integer value of 1 being passed as a tag name). It is a contract defining what type of data can be sent to the API.

Lastly, we will create a POST route or path that will be listening for HTTP POST requests sent from our device. This might be difficult to understand at first glance, so please focus on the critical elements. The @app.post(“/tags”) decorator instructs our API application to listen for HTTP POST requests of our device's payload at the endpoint URI of “tags” appended to our base server URI. This will make more sense as we run the application.

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

 

Altogether, the main.py script should resemble:

from fastapi import FastAPI
from pydantic import BaseModel

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

app = FastAPI()

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

 

API Runtime

Now we can run the server that will host our API. The server in this case will be localhost using the Uvicorn implementation. FastAPI is built on top of Uvicorn, and when we used the “[all]” extra requirement specifier in the pip install earlier, Uvicorn was installed as a dependency. To start the server enter the following in the command line. If you named your file other than main.py the “main” argument below should be replaced with your respective module name:

uvicorn main:app -–reload

 

The reload flag allows the server to automatically reload if you make changes to the main.py code and save. This saves time from having to manually stop and start the server again. If the startup is successful, the below should show:

Startup success

 

Note the first line, “Uvicorn running on...”. That URI is the API server that we will append the “/tags” endpoint to in our device.py script in gray bold below:

# Necessary libraries. Requests will require a pip install
import requests
from random import randint
from time import sleep

APIServer = "http://127.0.0.1:8000/tags" # Uvicorn local server running FastAPI instance + Post endpoint

# Scanning/refresh rate behavior created by while loop
while True:
    # Randomly create our read only Tag A
    tag = "Tag A"
    tagValue = randint(1,100)

    # Construct a JSON payload body for Tag A
    payload = {"tagName": tag,
               "value": tagValue
    }

    # Send the payload via an HTTP request to the API Server
    response = requests.post(APIServer, json = payload)

    # Handle the response from the API Server
    if response.status_code == 200:
        print(response.content.decode('utf-8'))
    else:
        print(response.status_code, response.content.decode('utf-8'))

    # Time delay of 5 seconds
    sleep(5)

 

The last step is to initiate our device.py script using the below in a new terminal window:

python device.py

 

The device.py script should receive a response from the main.py script running on the Uvicorn server if the request was successful:

Tag entry verification

 

Flipping over to the Uvicorn terminal, the INFO message should appear indicating a 200 HTTP status code:

HTTP status code

 

Review and Next Steps

We have successfully created a bridge between our device and the API server for the device to push data to the server. Systematically, this looks like below:

 

Currently completed system

Figure 3. Solution architecture now completed at end of part 2. Image provided by the author

 

Now, the goal is to extract the data being posted to the API server onto a front-end HMI. The next section, Part 3 of this series, will continue onward to the front-end integration.