ScadaPy сервер JSON

    В продолжение предыдущих статей о применении python для построения собственной scada системы, хотелось бы описать способ организации обмена между устройствами и вывод данных посредством json — текстового формата обмена данными.

    В данном случае будем использовать клиентские части modbusTCP и OPCUA библиотек.
    В итоге у нас получится http сервер, работающий в качестве master для подчиненных устройств, которые в свою очередь работают в режиме slave.

    image

    Modbus master


    Для настройки работы мастера modbus TCP, импортируем необходимые библиотеки:

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

    Необходимо выполнить инициализацию мастера с указанием ip адреса и порта, а также timeout ожидания ответа:

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

    Описываем циклическую функцию опроса slave устройств, с указанием названий регистров и адресов ячеек:

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

    В результате будет запущен циклический опрос подчиненного устройства по протоколу modbusTCP с IP адресом 127.0.0.1 и портом 502. Читаться будет регистр READ_INPUT_REGISTERS и в переменную data будет записано значение находящееся по адресу 0х00.

    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 серверах жесткая иерархия наследования, существует точное определение parent и child, поэтому можно строить довольно сложные системы с большим количеством вложенных объектов. Но нам, в данном случае, такое количество функций на сегодняшний день не понадобилось, поэтому мы ограничились созданием узла в корневой папке Objects и присвоением ему значения. Получилось приблизительно так 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, в st записывается StatusCode, ts и tsc записываются временные метки ServerTimestamp и SourceTimestamp соответственно.

    Для опроса подчиненных устройств используется также циклический опрос, запущенный в отдельном потоке thread, хотя правильнее было сделать подписку на событие.

    Web сервер Json


    Для создания web сервера потребуются библиотеки:

    from http.server import BaseHTTPRequestHandler, HTTPServer
    import json
    import base64
    Сам сервер запустить несложно, всего две команды, в сети существует большое количество описаний и примеров.

    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 серверу, но с открытым доступом, а хотелось бы установить какую-нибудь авторизацию, доступ по логину и паролю.
    Как оказалось это не особо сложно сделать используя headers.

    Пример
    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'))


    Если теперь попробовать подключиться посредством браузера, то авторизация выполняется и данные передаются, но получать данные из браузера без парсера не самая хорошая идея, мы предполагали получать данные методом GET с помощью JavaScrypt и функции XMLHttpRequest(), используя сценарий в странице html. Но при такой реализации браузер сначала отправляет запрос не методом GET, а методом OPTIONS и должен получить response = 200, только после этого будет выполнен запрос методом GET.

    Добавили еще функцию:

    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


    Ниже приводится примерное описание настройки конфигуратора для запуска скриптов.

    Внешний вид окна и назначение кнопок управления:

    image

    Допустим, стоит задача получать данные от датчика температуры с параметрами:
    Протокол: 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, добавляем новый сервер кнопка (+) слева вверху, указываем название и сохраняем.

    Теперь, нужно оформить созданный instance и ввести параметры web сервера.

    image

    Записываем параметры опроса подчиненного устройства, в данном случае датчика температуры:

    image

    После этого, при нажатии кнопки сохранить скрипт, в папке scr появится файл с названием web_(номер нашего сервера в базе).bat для Windows или web_(номер нашего сервера в базе).sh для Linux. В этом файле будут прописаны пути запуска скрипта.

    В данном случае пример для 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

    Можно запустить скрипт сразу на выполнение, нажав кнопку расположенную рядом с кнопкой сохранения (все кнопки снабжены всплывающими подсказками).

    После запуска появится консольное окно с информацией о запуске и подключениях.

    image

    Теперь, запустив браузер, пишем строку подключения _https://192.168.0.103:8080/all, а после ввода пароля видим следующее в Chrome:

    image

    Или в Firefox:

    image

    А в консоли запущенного сервера будет выведена информация о сессиях подключения:

    image

    В данном случае мы получаем данные по всем переменным настроенным на сервере, поскольку в запросе GET ввели параметр all. Это не совсем правильно, поскольку при увеличении количества переменных придется получать и обрабатывать данные, которые в текущий момент не используются, поэтому лучше вводить непосредственное имя переменной, значение которой необходимо обработать: 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;
                 
                  }
            }
     }
    

    Пример отображения полученных данных в различных виджетах.

    image

    При получении данных от OPCUA сервера, структура JSON ответа немного изменится, но незначительно. В любом случае разобраться там не составит труда.

    Ссылка для скачивания на github
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.