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

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

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

    Подробнее
    Реклама
    Комментарии 10
    • +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;
      


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

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

      • 0
        Посоветуйте, какую библиотеку лучше использовать?
        • 0
          Придется править самому код ModbusRTU.h или дождаться пока я выложу нашу реализацию на гитхаб. В нашей нет пока работы с мастером, ибо нам пока не надо. В принципе могу выложить код для слейвов
          • +1
            Держите ссылку Modbus slave for Arduino
            • 0
              Спасибо большое
              • 0
                По интерфейсу данная реализация — один в один то что использовали Вы. Так что Ваш код даже править не придется вообще.

                Из отличий ещё — поддержка только хардварного UART. Думаю нет смысла заморачиваться на софт сериал
          • 0
            Спасибо за комментарий, буду это учитывать в дальнейшем.
            Конечно хотелось бы использовать более стабильно работающую библиотеку.
            • 0
              Сударь, вам респект, вы сэкономили мне кучу времени
            • 0
              Да, совершенно забыл вот что. 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++)
               {
              
              
               }       
              


              Так что использовать такое даже в хобби-проектах или, упаси боже, в продакшине не очень разумно
              • 0
                а что у вас формат такой в setting.cfg ???

                можно же просто сохранять словари и загружать через import

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