python

Простая Scada на Python и Arduino

  • среда, 11 октября 2017 г. в 03:11:36
https://habrahabr.ru/post/339800/
  • Python


В продолжение статьи о возможности построения собственной scada системы на языке Python, хочу предложить вариант практического применения.
Возникла необходимость контроля температуры воздуха в серверном помещении предприятия.
Такая проблема существует на малых предприятиях ввиду ограниченности количества персонала и технических средств.
Проблема конечно не глобального масштаба, но, как правило, на подобных предприятиях серверное оборудование располагают в небольших помещениях, иногда в бывших коптерках или хозяйственных комнатах.
Разумеется, для эффективного охлаждения оборудования там устанавливают кондиционер.
Но вот этот самый кондиционер имеет свойство ломаться, как объясняют ремонтники, то «конденсатор перегорел», то «фреон закончился».
После таких ЧП у IT инженеров возникает множество проблем, кто сталкивался с этим, тот поймет.
Задача не является сложной, к тому же в сети существует много примеров реализации.
Для данной цели решено было воспользоваться Arduino UNO и датчиком температуры DS18b20.

image

Прочитав статью, загрузил в Arduino
программу.
#include "ModbusRtu.h"
#include <OneWire.h>
#define ID   10     // адрес ведомого

Modbus slave(ID, 0, 0); 
// массив данных modbus
uint16_t au16data[20];
const int analogInPin = A0;
int8_t state = 0;
int DS18S20_Pin = 2; //DS18S20 Signal pin on digital 2
OneWire ds(DS18S20_Pin);  // on digital pin 2
int tmp =0;
void setup() {
  
  // настраиваем последовательный порт ведомого
  slave.begin( 9600 ); 
  // зажигаем светодиод на 100 мс
 
}



void loop() {
   float temperature = getTemp();
  tmp= temperature * 10;
  au16data[2] = tmp;
 
  state = slave.poll( au16data, 11);  
   
  delay(10);
 
} 


float getTemp(){
  //returns the temperature from one DS18S20 in DEG Celsius
 
  byte data[12];
  byte addr[8];
 
  if ( !ds.search(addr)) {
      //no more sensors on chain, reset search
      ds.reset_search();
      return -1000;
  }
 
  if ( OneWire::crc8( addr, 7) != addr[7]) {
      Serial.println("CRC is not valid!");
      return -1000;
  }
 
  if ( addr[0] != 0x10 && addr[0] != 0x28) {
      Serial.print("Device is not recognized");
      return -1000;
  }
 
  ds.reset();
  ds.select(addr);
  ds.write(0x44,1); // start conversion, with parasite power on at the end
 
  byte present = ds.reset();
  ds.select(addr);    
  ds.write(0xBE); // Read Scratchpad
 
   
  for (int i = 0; i < 9; i++) { // we need 9 bytes
    data[i] = ds.read();
  }
   
  ds.reset_search();
   
  byte MSB = data[1];
  byte LSB = data[0];
 
  float tempRead = ((MSB << 8) | LSB); //using two's compliment
  float TemperatureSum = tempRead / 16;
   
  return TemperatureSum;
   
}



Теперь Arduinо выступает в роли Slave устройства с адресом 10 и работает по протоколу modbus RTU. Помимо этого, программа в постоянном цикле опрашивает датчик температуры DS18b20 и записывает текущие показания по адресу 2 регистра READ_INPUT_REGISTERS.
Поскольку Slave устройство соединяется с компьютером по USB интерфейсу с выделенным com портом, то для получения данных от него можно воспользоваться программой
modbus_rtu.py.

#!/usr/bin/env python
import sys
import time
import logging
import modbus_tk
import modbus_tk.defines as cst
import modbus_tk.modbus_tcp as modbus_tcp
from modbus_tk import modbus_rtu
import serial
logger = modbus_tk.utils.create_logger("console")


if __name__ == "__main__":

     serverSlave=''
     portSlave=0
     param = []
     reg=[]
     startAdr=[]
     rangeAdr=[]
     setFrom=[]
     setRange=[]
     rtuAddress=[]
     units=0
     try:
         count=0
         param = []

         i=0
         for _ in range(256):
             param.append(i)
             reg.append(i)
             startAdr.append(i)
             rangeAdr.append(i)
             setFrom.append(i)
             setRange.append(i)
             rtuAddress.append(i)

             i = i + 1
         with open('setting.cfg') as f:
             for line in f:
                 param[count]=line.split(';')
                 if(param[count][0]=='server'):
                     serverSlave= param[count][1]
                     portSlave =  param[count][2]

                 if(param[count][0]=='cport'):
                     serialPort= param[count][1]


                 if(param[count][0]=='rtu'):
                         rtuAddress[count] = param[count][1]
                         reg[count]  = param[count][2]
                         startAdr[count] = param[count][3]
                         rangeAdr[count] = param[count][4]
                         setFrom[count] = param[count][5]
                         setRange[count] = param[count][6]
                         count=count + 1
                         units=count



             server = modbus_tcp.TcpServer(address=serverSlave, port=int(portSlave) )
             server.start()
             slave = server.add_slave(1)

             slave.add_block('0', cst.COILS, 0, 1000)
             slave.add_block('1', cst.DISCRETE_INPUTS, 0, 1000)
             slave.add_block('2', cst.ANALOG_INPUTS, 0, 1000)
             slave.add_block('3', cst.HOLDING_REGISTERS, 0, 1000)
             f.close()
             serialPort=serial.Serial(port=serialPort, baudrate=9600, bytesize=8, parity='N', stopbits=1, xonxoff=0)
             master = modbus_rtu.RtuMaster( serialPort )
             master.set_timeout(1.0)

     except IOError as e:
         print "I/O error({0}): {1}".format(e.errno, e.strerror)

     try:
         print 'Starting server...'
         while True:

             i=0
             for i in range(units):




                 if(reg[i] == 'READ_INPUT_REGISTERS'):
                     dataRIR=[]
                     for c in range(0, int(rangeAdr[i]) ):
                         dataRIR.append(c)
                         c+=1

                     try:
                         dataRIR= master.execute(int(rtuAddress[i]), cst.READ_INPUT_REGISTERS, int(startAdr[i]), int(rangeAdr[i])  )
                         slave.set_values('2', int(setFrom[i]), dataRIR)
                         serialPort.flushInput()
                         serialPort.flushOutput()
                         serialPort.flush()

                         print 'rtu' , rtuAddress[i],'READ_INPUT_REGISTERS',dataRIR
                     except:
                         for c in range(0,int(rangeAdr[i])  ):
                             dataRIR[c] = 0
                             c+=1

                         print 'rtu' , rtuAddress[i],'READ_INPUT_REGISTERS','Fail to connect',dataRIR
                         slave.set_values('2', int(setFrom[i]), dataRIR)


                 if(reg[i] == 'READ_DISCRETE_INPUTS'):
                     dataRDI=[]
                     for c in range(0, int(rangeAdr[i]) ):
                         dataRDI.append(c)
                         c+=1
                     try:
                         dataRDI= master.execute(int(rtuAddress[i]), cst.READ_DISCRETE_INPUTS, int(startAdr[i]), int(rangeAdr[i])  )
                         slave.set_values('1', int(setFrom[i]), dataRDI)
                         serialPort.flushInput()
                         serialPort.flushOutput()
                         serialPort.flush()

                         print  'rtu' , rtuAddress[i],'READ_DISCRETE_INPUTS',dataRDI
                     except:
                         for c in range(0,int(rangeAdr[i])  ):
                             dataRDI[c] = 0
                             c+=1
                         print 'rtu' , rtuAddress[i],'READ_DISCRETE_INPUTS','Fail to connect' ,dataRDI,len(dataRDI)
                         slave.set_values('1', int(setFrom[i]), dataRDI)


                 if(reg[i] == 'READ_COILS'):
                     dataRC=[]
                     for c in range(0, int(rangeAdr[i]) ):
                         dataRC.append(c)
                         c+=1
                     try:
                         dataRC= master.execute(int(rtuAddress[i]), cst.READ_COILS, int(startAdr[i]), int(rangeAdr[i])  )
                         slave.set_values('0', int(setFrom[i]), dataRC)
                         serialPort.flushInput()
                         serialPort.flushOutput()
                         serialPort.flush()

                         print  'rtu' , rtuAddress[i],'READ_COILS',dataRC
                     except:
                         for c in range(0,int(rangeAdr[i])  ):
                             dataRC[c] = 0
                             c+=1
                         slave.set_values('0', int(setFrom[i]), dataRC)
                         print 'rtu' , rtuAddress[i],'READ_COILS','Fail to connect',dataRC

                 if(reg[i] == 'READ_HOLDING_REGISTERS'):
                     dataRHR=[]
                     for c in range(0, int(rangeAdr[i]) ):
                         dataRHR.append(c)
                         c+=1
                     try:
                         dataRHR= master.execute(int(rtuAddress[i]), cst.READ_HOLDING_REGISTERS, int(startAdr[i]), int(rangeAdr[i])  )
                         slave.set_values('3', int(setFrom[i]), dataRHR)
                         serialPort.flushInput()
                         serialPort.flushOutput()
                         serialPort.flush()
                         print  'rtu' ,rtuAddress[i],'READ_HOLDING_REGISTERS',dataRHR

                     except:
                         for c in range(0,int(rangeAdr[i])  ):
                             dataRHR[c] = 0
                             c+=1
                         slave.set_values('3', int(setFrom[i]), dataRHR)
                         print 'rtu ', rtuAddress[i],'READ_HOLDING_REGISTERS','Fail to connect',dataRHR

             time.sleep(0.1)

     except modbus_tk.modbus.ModbusError, e:
         logger.error("%s- Code=%d" % (e, e.get_exception_code()))


С одной стороны эта программа является Master для опроса подчиненных устройств по протоколу modbus RTU, а с другой является Slave устройством и передает данные на верхний уровень по протоколу modbus TCP.

image

Программа master_rtu.py используется в случае, если приходится собирать
показания с нескольких устройств по протоколу modbus RTU и/или интерфейсу rs485.
В файле конфигурации указывается адрес com порта и rtu адреса slave устройств.
Кроме того указываются регистры опроса и адреса регистров, в которые записываются полученные данные.

Описание файла настроек setting.cfg для master_rtu.py:

server;192.168.0.200;507; # 
    # server - идентификатор переменной
    # 192.168.0.200 - IP адрес slave части modbus TCP для входящих подключений
    # 507 - Порт slave части modbus TCP для входящих подключений

cport;COM5; # 
    # cport - идентификатор переменной
    # COM5 - адрес СОМ порта для опроса терминальных устройств по протоколу modbusRTU

rtu;10;READ_INPUT_REGISTERS;0;10;0;0;comment
    # rtu - идентификатор переменной
    # 10 - rtu адрес slave устройства куда подключаемся
    # READ_INPUT_REGISTERS -регистр для чтения slave устройства куда подключаемся
    # варианты: 
        # READ_DISCRETE_INPUTS
        # READ_COILS 
        # READ_HOLDING_REGISTERS 
    # 2 - стартовый адрес регистра с которого начинается чтение данных на slave устройстве modbus RTU
    # 1 - количество адресов регистра которые считываются на slave устройстве modbus RTU
    # 0 - стартовый адрес размещения полученных данных на slave части утилиты  modbus TCP
    # comment - комментарий 

В данной конфигурации будет опрашиваться modbus RTU Slave устройство с адресом 10.
В регистре READ_INPUT_REGISTERS по адресу 2 будет прочитано значение измеренной температуры и записано в регистр READ_INPUT_REGISTERS по адресу 0 slave части программы для опроса по modbus TCP.

image

В файле настроек аналоговых сигналов ai.cfg записываем:

ai;1;100;100;green;0.1;50;Air Temp A;ameter;

Т.е. будем брать измеренное значение температуры регистра READ_INPUT_REGISTERS по адресу 0х00, размещать на canvas в координатах x=100, y=100 и отображать с помощью стрелочного объекта мнемосхемы.

В файле настроек settings.cfg для scada.py пишем:

slaveIP=192.168.0.200 -- ip адрес modbus TCP slave устройства 
slavePort=504 -- порт modbus TCP slave устройства
discretCfg=di.cfg
coilCfg=ci.cfg
analogCfg=ai.cfg
buttonCfg=bt.cfg
bgimage=bg.gif
delayTime=500
debug=False

Результаты измерений можно вывести на различные объекты мнемосхемы,
в том числе осуществлять контроль на динамическом графике.

image

Исходный код можно скачать здесь здесь.