πŸ“œ ⬆️ ⬇️

ScadaPy JSON server

In continuation of the previous articles on the use of python to build your own scada system, I would like to describe the method of organizing the exchange between devices and data output through json - a text format for exchanging data.

In this case, we will use the client parts of the modbusTCP and OPCUA libraries.
As a result, we will have an http server that acts as a master for the slave devices, which in turn work in the slave mode.

image
')

Modbus master


To configure the modbus TCP wizard, import the necessary libraries:

import modbus_tk import modbus_tk.defines as cst import modbus_tk.modbus_tcp as modbus_tcp 

It is necessary to perform the initialization of the wizard with the indication of the ip address and port, as well as timeout waiting for a response:

 master = modbus_tcp.TcpMaster(host='127.0.0.1', port=502) master.set_timeout(2) 

We describe the cyclic function of polling slave devices, with the names of registers and cell addresses:

 def getModbus(): while True: try: data= master.execute(rtu, cst.READ_INPUT_REGISTERS,0,1 ) except Exception as e: print (e) time.sleep(1) #rtu –  RTU modbus # cst.READ_INPUT_REGISTERS –  ,       : #cst.READ_INPUT_REGISTERS #cst.READ_DISCRETE_INPUTS #cst.READ_COILS #cst.READ_HOLDING_REGISTERS 

Now you need to run the poll loop in a separate thread thread :

 modb = threading.Thread(target=getModbus) modb.daemon = True modb.start() 

As a result, a cyclic polling of the slave device will be launched using the modbusTCP protocol with the IP address 127.0.0.1 and port 502. The READ_INPUT_REGISTERS register will be read and the value located at the 0x00 address will be written to the data variable.

OPCUA client


To receive data from the OPCUA server, you must connect the library freeopcua

  from opcua import ua, Client 
and create a new client connection:

 url="opc.tcp://127.0.0.1:4840/server/" try: client = Client(url) client.connect() root = client.get_root_node() except Exception as e: print(e) 

In OPC servers there is a strict hierarchy of inheritance, there is an exact definition of parent and child , so you can build quite complex systems with a large number of nested objects. But we, in this case, did not need such a number of functions today, so we limited ourselves to creating a node in the root Objects folder and assigning it a value. It came out roughly like Objects -> MyNode -> MyNodeValue , but I must admit that this method is not acceptable for building more complex systems.

 obj = root.get_child(["0:Objects"]) objChild= obj.get_children() for i in range(0,len(objChild)): unitsChild.append(i) unitsChild[i]=objChild[i].get_children() parName=val_to_string(objChild[i].get_browse_name())[2:] for a in range(0, len( unitsChild[i] ) ): valName=val_to_string(unitsChild[i][a].get_browse_name())[2:] try: valData=unitsChild[i][a].get_value() data =unitsChild[i][a].get_data_value() st=val_to_string(data.StatusCode) ts= data.ServerTimestamp.isoformat() tsc= data.SourceTimestamp.isoformat() except Exception as e: print(e) 

The variable value itself can be seen in valData , the StatusCode is written to st , the ts and tsc are recorded with the ServerTimestamp and SourceTimestamp timestamps, respectively.

To poll the slave devices, cyclic polling is also used, running in a separate thread, although it was more correct to subscribe to the event.

Json Web Server


To create a web server, you need libraries:

 from http.server import BaseHTTPRequestHandler, HTTPServer import json import base64 
The server itself is easy to start, just two commands, there are a large number of descriptions and examples on the network.

 server_address = (β€œ127.0.0.1”, 8080) httpd = server_class(server_address, handler_class) try: httpd.serve_forever() except Exception as e: print(e) httpd.server_close() 

The most interesting thing started later, when for testing it became necessary to connect from the Chrome or Firefox browser to the created server.

Constantly popped refuse_connect .

A little search on the network, we found a solution - you need to add to the do_GET function:

 self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Credentials', 'true') 

Now I managed to get access to a working web server, but with open access, but I would like to establish some kind of authorization, access by login and password.
As it turned out, it is not particularly difficult to do using headers.

Example
 def do_GET(self): global key if self.headers.get('Authorization') == None: self.do_AUTHHEAD() response = { 'success': False, 'error': 'No auth header received'} self.wfile.write(bytes(json.dumps(response), 'utf-8')) elif self.headers.get('Authorization') == 'Basic ' + str(key): resp=[] self.send_response(200) self.send_header('Allow', 'GET, OPTIONS') self.send_header("Cache-Control", "no-cache") self.send_header('Content-type','application/json') self.send_header('Access-Control-Allow-Origin', 'null') self.send_header('Access-Control-Allow-Credentials', 'true') self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'X-Request, X-Requested-With') self.send_header("Access-Control-Allow-Headers", "Authorization") self.end_headers() req=str(self.path)[1:] if(req == "all" ): try: for i in range(0,units): resp.append({varName[i]:[reg[i],varNameData[i]]}) i+=1 self.wfile.write(json.dumps( resp ).encode()) except Exception as e: print('all',e) else: for i in range(0,units): if(req == varName[i] ): try: resp =json.dumps({ varName[i]:varNameData[i] } ) self.wfile.write(resp.encode()) except Exception as e: print(e) i+=1 else: self.do_AUTHHEAD() response = { 'success': False, 'error': 'Invalid credentials'} self.wfile.write(bytes(json.dumps(response), 'utf-8')) 


If you now try to connect using a browser, then authorization is performed and data is transmitted, but getting data from a browser without a parser is not a good idea, we assumed to get data using the GET method using JavaScrypt and the XMLHttpRequest () function using the script in the html page. But with this implementation, the browser first sends the request not by the GET method, but by the OPTIONS method and must receive a response = 200, only after that the request will be executed by the GET method.

Added another function:

 def do_OPTIONS(self): self.send_response(200) self.send_header('Access-Control-Allow-Credentials', 'true') self.send_header('Access-Control-Allow-Origin', 'null') self.send_header('Access-Control-Allow-Methods', 'GET,OPTIONS') self.send_header('Access-Control-Allow-Headers', 'X-Request, X-Requested-With') self.send_header("Access-Control-Allow-Headers", "origin, Authorization, accept") self.send_header('Content-type','application/json') self.end_headers() 

When this function is enabled, the check will be performed via 'Access-Control-Allow-Origin' and, if it is not set to 'null' , there will be no exchange.

Now we have access by login and password, the browser will exchange data according to the script, but it is desirable to organize SSL data encryption. To do this, create an SSL certificate file and add the following line before starting the server:

 httpd.socket = ssl.wrap_socket (httpd.socket, certfile=pathFolder+'json_server.pem',ssl_version=ssl.PROTOCOL_TLSv1, server_side=True) 

Of course, this is a self-signed certificate, but in any case it is better than an open protocol.

To process the data in the script on the html page, you can use the above function XMLHttpRequest ():

Example
 xmlhttp=new XMLHttpRequest(); xmlhttp.open("GET","http://192.168.0.103:8080/all",true); xmlhttp.setRequestHeader("Authorization", "Basic " + btoa(login+":"+password)); xmlhttp.withCredentials = true; xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xmlhttp.send(null); xmlhttp.onreadystatechange=function() { if (xmlhttp.readyState==4 && xmlhttp.status==200) { resp= xmlhttp.responseText; parseResp=JSON.parse(resp); } } 


JSON configurator description


Below is a rough description of configuring the configurator to run scripts.

The appearance of the window and the appointment of control buttons:

image

Suppose the task is to receive data from a temperature sensor with parameters:
Protocol: modbusTCP
IP Address: 192.168.0.103
Port: 502
RTU: 1
Register: READ_INPUT_REGISTERS (0x04)
Address: 0
Variable name: tempSensor_1
Print this data on the json server:
Format: json
IP Address: 192.168.0.103
Port: 8080
Login: 111
Password: 222

Run json.py , add a new server button (+) at the top left, specify the name and save.

Now, you need to arrange the created instance and enter the parameters of the web server.

image

We write down the parameters for polling the slave, in this case the temperature sensor:

image

After that, when you click the save script button, a file called web_ (our server number in the database) .bat for Windows or web_ (our server number in the database) .sh for Linux appears in the scr folder. The path to the script will be written in this file.

In this case, the example for Windows is the web_15.bat file:

rem 'ScadaPy Web JSON v.3.14'
rem Web ' '
rem Http '192.168.0.103'
rem Http '8080'
start c:\Python35\python.exe F:\scadapy\main\source\websrv.py 15 F:\scadapy\main\db\webDb.db

You can launch the script immediately for execution by clicking the button located next to the save button (all buttons are provided with tooltips).

After launch, a console window will appear with information about startup and connections.

image

Now, having started the browser, we write the connection string _https: //192.168.0.103: 8080 / all , and after entering the password we see the following in Chrome:

image

Or in Firefox:

image

And in the console of the running server information about connection sessions will be displayed:

image

In this case, we get data on all variables configured on the server, because in the GET request we entered the parameter all . This is not entirely correct, since increasing the number of variables will have to receive and process data that is not currently being used, so it’s better to enter the immediate name of the variable whose value you want to process: tempSensor_1 .

In this case:
Request - tempSensor_1
The answer is {"tempSensor_1": [2384]}

JavaScript processing


I want to describe a little how to embed the formation of the request and the processing of the response in the html page.

You can use the XMLHttpRequest () function to execute a request, although there are currently other ways to connect. Upon successful connection and getting the status equal to 200, it is enough to execute the JSON.parse () function.
To establish the cyclical nature of the query, you must start a timer.

Example.
  function getTemp() { var dataReq='tempSensor_1'; var login='111', passw='222'; var ip='192.168.0.103'; var port='8080'; if (window.XMLHttpRequest) { xmlhttp=new XMLHttpRequest(); } else { xmlhttp=new ActiveXObject("Microsoft.XMLHTTP"); } xmlhttp.open("GET","https://"+ip+":"+port+"/"+dataReq,true); xmlhttp.setRequestHeader("Authorization", "Basic " + btoa(login+":"+passw)); xmlhttp.withCredentials = true; xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xmlhttp.send(null); xmlhttp.onreadystatechange=function() { if (xmlhttp.readyState==4 && xmlhttp.status==200) { resp= xmlhttp.responseText; parseResp=JSON.parse(resp); data=parseResp.tempSensor_1[0]; log("Val :" + data +"\n"); resp=data*0.1; } } } 

An example of the display of the data in various widgets.

image

When receiving data from the OPCUA server, the structure of the JSON response will change slightly, but only slightly. In any case, it will not be difficult to figure it out.

Download link on github

Source: https://habr.com/ru/post/343302/


All Articles