В продолжение статьи о возможности построения собственной scada системы на языке Python, хочу предложить вариант практического применения.
Возникла необходимость контроля температуры воздуха в серверном помещении предприятия.
Такая проблема существует на малых предприятиях ввиду ограниченности количества персонала и технических средств.
Проблема конечно не глобального масштаба, но, как правило, на подобных предприятиях серверное оборудование располагают в небольших помещениях, иногда в бывших коптерках или хозяйственных комнатах.
Разумеется, для эффективного охлаждения оборудования там устанавливают кондиционер.
Но вот этот самый кондиционер имеет свойство ломаться, как объясняют ремонтники, то «конденсатор перегорел», то «фреон закончился».
После таких ЧП у IT инженеров возникает множество проблем, кто сталкивался с этим, тот поймет. Задача не является сложной, к тому же в сети существует много примеров реализации. Для данной цели решено было воспользоваться Arduino UNO и датчиком температуры DS18b20.
Прочитав статью, загрузил в Arduino
Теперь Arduinо выступает в роли Slave устройства с адресом 10 и работает по протоколу modbus RTU. Помимо этого, программа в постоянном цикле опрашивает датчик температуры DS18b20 и записывает текущие показания по адресу 2 регистра READ_INPUT_REGISTERS.
Поскольку Slave устройство соединяется с компьютером по USB интерфейсу с выделенным com портом, то для получения данных от него можно воспользоваться программой:
С одной стороны эта программа является Master для опроса подчиненных устройств по протоколу modbus RTU, а с другой является Slave устройством и передает данные на верхний уровень по протоколу modbus TCP.
Программа master_rtu.py используется в случае, если приходится собирать показания с нескольких устройств по протоколу modbus RTU и/или интерфейсу rs485. В файле конфигурации указывается адрес com порта и rtu адреса slave устройств. Кроме того указываются регистры опроса и адреса регистров, в которые записываются полученные данные.
Описание файла настроек setting.cfg для master_rtu.py:
В данной конфигурации будет опрашиваться modbus RTU Slave устройство с адресом 10. В регистре READ_INPUT_REGISTERS по адресу 2 будет прочитано значение измеренной температуры и записано в регистр READ_INPUT_REGISTERS по адресу 0 slave части программы для опроса по modbus TCP.
В файле настроек аналоговых сигналов ai.cfg записываем:
Т.е. будем брать измеренное значение температуры регистра READ_INPUT_REGISTERS по адресу 0х00, размещать на canvas в координатах x=100, y=100 и отображать с помощью стрелочного объекта мнемосхемы.
В файле настроек settings.cfg для scada.py пишем:
Результаты измерений можно вывести на различные объекты мнемосхемы, в том числе осуществлять контроль на динамическом графике.
Исходный код можно скачать здесь здесь.
Возникла необходимость контроля температуры воздуха в серверном помещении предприятия.
Такая проблема существует на малых предприятиях ввиду ограниченности количества персонала и технических средств.
Проблема конечно не глобального масштаба, но, как правило, на подобных предприятиях серверное оборудование располагают в небольших помещениях, иногда в бывших коптерках или хозяйственных комнатах.
Разумеется, для эффективного охлаждения оборудования там устанавливают кондиционер.
Но вот этот самый кондиционер имеет свойство ломаться, как объясняют ремонтники, то «конденсатор перегорел», то «фреон закончился».
После таких ЧП у 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
Результаты измерений можно вывести на различные объекты мнемосхемы, в том числе осуществлять контроль на динамическом графике.
Исходный код можно скачать здесь здесь.
Комментарии (8)
maisvendoo
11.10.2017 07:09Да, совершенно забыл вот что. ModbusRTU.h худо-бедно работает как слейв, но в коде, касающемся мастера есть вот такие милые вещи
/** * This method processes functions 1 & 2 (for master) * This method puts the slave answer into master data buffer * * @ingroup register * TODO: finish its implementation */ void Modbus::get_FC1() { uint8_t u8byte, i; u8byte = 0; // for (i=0; i< au8Buffer[ 2 ] /2; i++) { // au16regs[ i ] = word( // au8Buffer[ u8byte ], // au8Buffer[ u8byte +1 ]); // u8byte += 2; // } }
и вот такие
u8regsno = u8bytesno = 0; // now auxiliary registers for (uint16_t i = 0; i < telegram.u16CoilsNo; i++) { }
Так что использовать такое даже в хобби-проектах или, упаси боже, в продакшине не очень разумно
maisvendoo
Я бы предостерег Вас от использования ModbusRTU.h
Дело в том, то эта библиотека выдерживает положенный по протоколу интервал тишины равный 5мс. При том что в спецификации указано, что интервал тишины должен быть не менее 3.5 символов. На скорости 9600 это около 3мс, на скорости же 115200 это уже 200мкс.
Продолжительность интервала тишины зависит от скорости!
Используя фиксированный интервал тишины разработчик этой библиотеки свел на нет эффект от увеличения скорости передачи. На скорости 115200 при нормальной реализации цикл «запрос-ответ» занимает порядка 2 мс, а нас тут заставляют 5 мс тишину выдерживать.
Я долго не мог понять почему увеличение скорости с 9600 до 115200 не дает эффекта, пока не ткнулся осциллографом. Ну а потом полез в код и увидел эту бяку
а вот так там задается интервал тишины
Для нашего проекта пришлось переписать эту библиотеку и результат ошеломил — быстродействие сети выросло просто колоссально. Скоро выложу новую либу на гит, а пока призываю: Люди! Смотрите код используемых библиотек, особенно если речь идет об ардуино!
Это не первый случай говнокода в их библиотеках…
fintler
Посоветуйте, какую библиотеку лучше использовать?
maisvendoo
Придется править самому код ModbusRTU.h или дождаться пока я выложу нашу реализацию на гитхаб. В нашей нет пока работы с мастером, ибо нам пока не надо. В принципе могу выложить код для слейвов
maisvendoo
Держите ссылку Modbus slave for Arduino
jackmas Автор
Спасибо большое
maisvendoo
По интерфейсу данная реализация — один в один то что использовали Вы. Так что Ваш код даже править не придется вообще.
Из отличий ещё — поддержка только хардварного UART. Думаю нет смысла заморачиваться на софт сериал
jackmas Автор
Спасибо за комментарий, буду это учитывать в дальнейшем.
Конечно хотелось бы использовать более стабильно работающую библиотеку.