https://habrahabr.ru/post/339800/В продолжение
статьи о возможности построения собственной scada системы на языке Python, хочу предложить вариант практического применения.
Возникла необходимость контроля температуры воздуха в серверном помещении предприятия.
Такая проблема существует на малых предприятиях ввиду ограниченности количества персонала и технических средств.
Проблема конечно не глобального масштаба, но, как правило, на подобных предприятиях серверное оборудование располагают в небольших помещениях, иногда в бывших коптерках или хозяйственных комнатах.
Разумеется, для эффективного охлаждения оборудования там устанавливают кондиционер.
Но вот этот самый кондиционер имеет свойство ломаться, как объясняют ремонтники, то «конденсатор перегорел», то «фреон закончился».
После таких ЧП у IT инженеров возникает множество проблем, кто сталкивался с этим, тот поймет.
Задача не является сложной, к тому же в сети существует много примеров реализации.
Для данной цели решено было воспользоваться Arduino UNO и датчиком температуры DS18b20.
Прочитав
статью, загрузил в 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.
Программа 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.
В файле настроек аналоговых сигналов
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
Результаты измерений можно вывести на различные объекты мнемосхемы,
в том числе осуществлять контроль на динамическом графике.
Исходный код можно скачать здесь
здесь.