ScadaPy JSONサーバー

pythonを使用して独自のscadaシステムを構築することに関する以前の記事の続きで、データ交換用のテキスト形式であるjsonを使用して、デバイスと出力データ間の交換を整理する方法について説明します。



この場合、 modbusTCPおよびOPCUAライブラリのクライアント部分を使用します。

その結果、スレーブのマスターとして機能するhttpサーバーを取得し、 スレーブモードで機能します。



画像



Modbusマスター



modbus TCPウィザードを構成するには、必要なライブラリをインポートします。



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





IPアドレスとポート、および応答を待つためのタイムアウトでウィザードを初期化する必要があります。



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





レジスタの名前とセルアドレスを示す、 スレーブデバイスのポーリングの周期機能について説明します。



 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
      
      





次に、別のスレッドthreadでポーリングサイクルを開始する必要があります



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





その結果、 IPアドレス 127.0.0.1およびポート502のmodbusTCPプロトコルを使用したスレーブデバイスの周期的なポーリングが開始され、READ_INPUT_REGISTERSレジスタが読み取られ、0x00にある値がデータ変数に書き込まれます。



OPCUAクライアント



OPCUAサーバーからデータを受信するには、 freeopcuaライブラリを接続する必要があります



  from opcua import ua, Client
      
      



新しいクライアント接続を作成します。



 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)
      
      





OPCサーバーには、継承の厳密な階層があり、 子の正確な定義があるため、多数のネストされたオブジェクトを含む非常に複雑なシステムを構築できます。 しかし、この場合、今日ではそのような多くの関数は必要ありませんでした。そのため、 Objectsのルートフォルダーにノードを作成して値を割り当てることに限定しました。 このようなオブジェクト-> MyNode-> MyNodeValueのようになりましたが、より複雑なシステムの構築にはこのメソッドは受け入れられないことを認めなければなりません。



 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)
      
      





変数の値はvalDataで直接確認でき、 StatusCodestに書き込まれ、 tsおよびtscはそれぞれServerTimestampおよびSourceTimestampのタイムスタンプ記録されます。



スレーブをポーリングする場合、ラウンドロビンポーリングも別のスレッドで実行されますが、イベントをサブスクライブする方が適切です。



Json Webサーバー



Webサーバーを作成するには、ライブラリが必要です。



 from http.server import BaseHTTPRequestHandler, HTTPServer import json import base64
      
      



サーバー自体を起動するのは難しくなく、コマンドは2つだけで、ネットワークには多くの説明と例があります。



 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()
      
      





最も興味深いことは、テストのためにChromeまたはFirefoxブラウザーから作成されたサーバーに接続する必要が生じたときに始まりました。



refuse_connectを常に表示します。



ネットワークを少し検索して、解決策を見つけました-do_GET関数に追加する必要があります:



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





これで、正常に機能するWebサーバーにアクセスできましたが、オープンアクセスでしたが、何らかの認証、ログインとパスワードによるアクセスを確立したいと思います。

判明したように、ヘッダーを使用してこれを行うのは特に難しいことではありません。



 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'))
      
      







ブラウザーを使用して接続しようとすると、認証が実行されてデータが転送されますが、パーサーなしでブラウザーからデータを受信することはお勧めできません。JavaScryptでGETメソッドを使用し、htmlページのスクリプトを使用してXMLHttpRequest()関数を使用してデータを受信することを想定しています。 しかし、そのような実装では、ブラウザーは最初にGETメソッドではなくOPTIONSメソッドによってリクエストを送信し、GETメソッドによってリクエストが実行された後にのみresponse = 200を受信する必要があります。



別の機能を追加しました:



 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()
      
      





この関数が接続されると、 「Access-Control-Allow-Origin」を使用してチェックが実行され、 「null」に設定されていない場合、交換は行われません。



ログインとパスワードでアクセスできるようになりました。ブラウザーはシナリオに従ってデータを交換しますが、SSLデータ暗号化を整理することをお勧めします。 これを行うには、サーバーを起動する前にSSL証明書ファイルを作成し、次の行を追加します。



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





もちろんこれは自己署名証明書ですが、いずれにしてもオープンプロトコルよりも優れています。



HTMLページのスクリプト内のデータを処理するには、上記のXMLHttpRequest()関数を使用できます。



 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の説明



以下は、スクリプトを実行するためのコンフィギュレーター設定の説明例です。



ウィンドウの外観とコントロールボタンの目的:



画像



タスクがパラメーター付きの温度センサーからデータを受信することであるとしましょう:

プロトコル: modbusTCP

IPアドレス: 192.168.0.103

ポート: 502

RTU: 1

登録: READ_INPUT_REGISTERS(0x04)

住所: 0

変数名: tempSensor_1

JSONサーバーでこのデータを印刷します。

フォーマット: json

IPアドレス: 192.168.0.103

ポート: 8080

ログイン: 111

パスワード: 222



json.pyを実行し、 左上に新しいサーバーボタン(+)を追加し、名前を指定して保存します。



次に、作成したインスタンスを作成し、Webサーバーのパラメーターを入力する必要があります。



画像



スレーブデバイスのポーリングパラメータ、この場合は温度センサーを書き留めます。



画像



その後、スクリプトの保存ボタンをクリックすると、Windowsの場合はweb_( データベース内のサーバーの数).batまたはLinuxの場合はweb_(データベース内のサーバーの数).shという名前のファイルがscrフォルダーに表示されます。 スクリプトの実行パスはこのファイルに書き込まれます。



この場合、Windowsの例、 web_15.batファイル:



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







保存ボタンの隣にあるボタンをクリックすると、すぐにスクリプトを実行できます(すべてのボタンにはツールチップが装備されています)。



起動後、起動と接続に関する情報を含むコンソールウィンドウが表示されます。



画像



ブラウザを起動したら 、接続文字列_https://192.168.0.103:8080 / allを書き込み、パスワードを入力すると、Chromeで次のように表示されます。



画像



またはFirefoxの場合:



画像



実行中のサーバーのコンソールには、接続セッションに関する情報が表示されます。



画像



この場合、パラメーターはすべて GETリクエストで入力されているため、サーバーで構成されているすべての変数のデータを取得します。 変数の数を増やすと、現在使用されていないデータを受信して​​処理する必要があるため、これは完全に正しいわけではありません。そのため、値を処理する必要がある変数の直接名を入力する方が良いでしょう: tempSensor_1



この場合:

リクエスト-tempSensor_1

答えは{"tempSensor_1":[2384]}です。



JavaScript処理



リクエストの形成とレスポンスの処理をhtmlページに統合する方法を少し説明したいと思います。



XMLHttpRequest()関数を使用してリクエストを実行できますが、現在利用可能な他の接続方法があります。 接続に成功し、ステータス200を取得したらJSON.parse()関数を実行するだけで十分です。

クエリ実行の周期性を確立するには、タイマーを実行する必要があります。



例。
  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; } } }
      
      





受信したデータをさまざまなウィジェットで表示する例。



画像



OPCUAサーバーからデータを受信すると、JSON応答の構造はわずかに変わりますが、わずかに変わります。 いずれにせよ、そこを理解することは難しくありません。



githubのダウンロードリンク



All Articles