В продолжение статьи о возможности построения собственной 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

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

Комментарии (8)


  1. maisvendoo
    10.10.2017 23:52
    +5

    Я бы предостерег Вас от использования ModbusRTU.h

    Дело в том, то эта библиотека выдерживает положенный по протоколу интервал тишины равный 5мс. При том что в спецификации указано, что интервал тишины должен быть не менее 3.5 символов. На скорости 9600 это около 3мс, на скорости же 115200 это уже 200мкс.

    Продолжительность интервала тишины зависит от скорости!

    Используя фиксированный интервал тишины разработчик этой библиотеки свел на нет эффект от увеличения скорости передачи. На скорости 115200 при нормальной реализации цикл «запрос-ответ» занимает порядка 2 мс, а нас тут заставляют 5 мс тишину выдерживать.

    Я долго не мог понять почему увеличение скорости с 9600 до 115200 не дает эффекта, пока не ткнулся осциллографом. Ну а потом полез в код и увидел эту бяку

    #define T35  5
    #define  MAX_BUFFER  64	//!< maximum size for the communication buffer in bytes
    


    а вот так там задается интервал тишины
    // check T35 after frame end or still no frame end
        if (u8current != u8lastRec)
        {
            u8lastRec = u8current;
            u32time = millis() + T35;
            return 0;
        }
        if (millis() < u32time) return 0;
    


    Для нашего проекта пришлось переписать эту библиотеку и результат ошеломил — быстродействие сети выросло просто колоссально. Скоро выложу новую либу на гит, а пока призываю: Люди! Смотрите код используемых библиотек, особенно если речь идет об ардуино!

    Это не первый случай говнокода в их библиотеках…


    1. fintler
      11.10.2017 09:36

      Посоветуйте, какую библиотеку лучше использовать?


      1. maisvendoo
        11.10.2017 13:18

        Придется править самому код ModbusRTU.h или дождаться пока я выложу нашу реализацию на гитхаб. В нашей нет пока работы с мастером, ибо нам пока не надо. В принципе могу выложить код для слейвов


      1. maisvendoo
        11.10.2017 13:31
        +1

        Держите ссылку Modbus slave for Arduino


        1. jackmas Автор
          11.10.2017 13:34

          Спасибо большое


          1. maisvendoo
            11.10.2017 13:36

            По интерфейсу данная реализация — один в один то что использовали Вы. Так что Ваш код даже править не придется вообще.

            Из отличий ещё — поддержка только хардварного UART. Думаю нет смысла заморачиваться на софт сериал


    1. jackmas Автор
      11.10.2017 10:01

      Спасибо за комментарий, буду это учитывать в дальнейшем.
      Конечно хотелось бы использовать более стабильно работающую библиотеку.


  1. 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++)
     {
    
    
     }       
    


    Так что использовать такое даже в хобби-проектах или, упаси боже, в продакшине не очень разумно