Pull to refresh

Как я писал библиотеку под МЭК 870-5-104 на Arduino при помощи Wireshark

Reading time 24 min
Views 45K
В этой статье я хотел бы рассказать о своем знакомстве с протоком передачи данных МЭК 870-5-104 со стороны контролируемого (slave) устройства путем написания простой библиотеки на Arduino.

Что такое МЭК 870-5-104 это и где применяется?


МЭК 60870-5-104 – протокол телемеханики, предназначенный для передачи сигналов ТМ в АСТУ, регламентирующий использование сетевого доступа по протоколу TCP/IP. Чаще всего применяется в энергетике для информационного обмена между энергосистемами, а также для получения данных от измерительных преобразователей (вольтметры, счетчики электроэнергии и прочее).

Стэк протокола МЭК 670-5-104:



Используемые материалы


  • плата Arduino UNO;
  • Ethernet shield (HR911105a);
  • в роли мастера МЭК 60870-5-104 будет выступать MicroScada от ABB;
  • Wireshark для анализа трафика.


Краткое описание этапов работы


  1. Установка TCP/IP соединение по 2404 порту;
  2. Подтверждение запроса на передачу данных (STARTDT act/con);
  3. Запрос на общий опрос станции;
  4. Подготовка и передача данных контролирующей (master) станции;
  5. Процедуры тестирования.

Подготовка


  • Подключена плата Arduino к ПК;
  • Настроен соответствующим образом сетевой интерфейс;
  • Настроена контролирующая (master) станция (добавлена 104 линия и добавлено контролируемое (slave) устройство).



Термины и сокращения


APCI — Управляющая Информация Прикладного Уровня может применяться как самостоятельный управляющий кадр (кадр U или кадр S).
ASDU — Блоки данных прикладного уровня, состоит из идентификатора блока данных и одного или более объектов информации, каждый из которых включает в себя один или более однородных элементов информации (либо комбинаций элементов информации).
APDU — Протокольный блок данных прикладного уровня.
ТС — телесигнализация.
ТИ — телеизмерения.
ТУ — телеуправление.

1. Установка TCP/IP соединение порт 2404


Контролирующая (master) станция инициализирует установку TCP соединения путем посылки TCP пакета с флагом (SYS). Соединение считается установленным, если в течение контрольного времени (t0) контролируемая станция (slave) выдала на свой уровень TCP/IP подтверждение «активного открытия» (SYS ACK). Контрольное время t0 называется «Тайм-аут установки соединения». Таймер t0 определяет, когда открытие отменяется и не определяет начало новой попытки соединения.



Взаимодействие с транспортным уровнем выполняет стандартная библиотека для плат Arduino «Ethernet.h». То есть первым делом необходимо установить TCP/IP соединение между контролируемой и контролирующей станциями. Для этого необходимо в скетче Arduino инициализировать устройство и создать сервер который будет ожидать входящие соединения через указанный порт.

Скетч
#include <Ethernet.h>
byte mac[] = {0x90, 0xA2, 0xDA, 0x0E, 0x94, 0xB7 };//мак адрес 
IPAddress ip(172, 16, 7, 1);// ip адрес контролируемого устройства
IPAddress gateway(172, 16,7, 0);//шлюз
IPAddress subnet(255, 255, 0, 0);//маска
EthernetClient client;
EthernetServer iec104Server(2404);// для МЭК 670-5-104- порт- 2404
void setup()
{
  Ethernet.begin(mac, ip, gateway, subnet); // инициализация Ethernet-устройства
}
void loop()
{
  client = iec104Server.available();//подсоединение клиентов 
}



Если загрузить этот скетч то будет происходить следующее:



Установка соединения, далее приходит пока неизвестный для Arduino пакет STARTDT act и по истечении определенного времени рвется соединение. Далее необходимо разобраться что такое STARTDT act.

2. Подтверждение запроса на передачу данных (STARTDT act/con)


В МЭК 670-5-104 существует 3 типа формата для передачи:

  • I-формат для передачи данных телеметрии;
  • S-формат для передачи квитанций;
  • U-формат для передачи посылок установления связи и тестирования канала связи.

После успешного «тройного рукопожатия» контролирующая (master) станция посылает APDU STARTDT (старт передачи данных). STARTDT инициирует для контролируемой (Slavе) станции разрешение передачи блоков ASDU (кадров I) в направлении контролирующей (master), для продолжения работы необходимо подтвердить STARTDT, если контролируемая (Slavе) станция готова к передаче блоков данных. Если контролируемая (slave) станция не подтверждает выполнение STARTDT то контролирующая (master) станция вызывает обязательное закрытие IP- соединения.

Картинка


Таким образом далее необходимо считать байты полученные от контролирующей (master) станции и разобрать их.

Скетч
uint8_t iec104ReciveArray[128];//массив для приема 
EthernetClient client = iec104Server.available();
 if(client.available())
  {
    delay(100);
    int i = 0;
while(client.available())
 {
    iec104ReciveArray[i] = client.read();//записываем в буфер приема данные
    i++;
 }


Прочитав данные необходимо разобрать их и сформировать ответ.

Wireshark


Вот как выглядит посылка содержащая блок STARTDT в программе Wireshark, APDU блок U-формата, который состоит только из APCI.
APCI-Управляющая Информация Прикладного Уровня может применяться как самостоятельный управляющий кадр (кадр U или кадр S).



Вкратце можно сказать, что блок APCI определяет тип блока APDU и его длину. APCI состоит из следующих шести байтов:

1. Признак инициализации блока APDU переменной длины, начинающийся байтом START2 68h;
2. Длин APDU, в данном примере равна четырем байтам;
3. Байт управления в котором определяется тип APDU, в данном примере записано значение равное семи, что означает запрос на передачу данных;
4,5,6 Не используются.



Исходя из вышеописанного, перед тем как ответить, не мешало бы определить какой тип APDU нам послала контролирующая станция. Зная, что тип APDU записан третьим по порядку чтения блока APCI байтом, сохраню его в целочисленную переменную.
Примечание: Если будет получен пакет формата «I» то 3 байт в APCI будет также содержать значение младшего слова счетчика принятых пакетов,

поэтому пришлось немного усложнить конструкцию определения типа APDU.

Скетч
ASDU=iec104ReciveArray[6];//пакет ASDU?
    switch (ASDU)
   {
    case 100://опрос станции
    TypeQuerry=iec104ReciveArray[2]-word(iec104ReciveArray[3],iec104ReciveArray[2]);//Тип 
    rxcnt+=2;//увеличение счетчика принятых пакетов 
    break;
    case 0:
    TypeQuerry=iec104ReciveArray[2]; //Тип 
    break;
    default :
    TypeQuerry=iec104ReciveArray[2];//Тип
    break;
   }      




Из рисунка выше видно, что тип APDU соответствующий значению 7 это STARTDT act соответственно ответить необходимо таким же по структуре пакетом только значение типа должно иметь значение 11 (0b), что соответствует STARTDT con.

Скетч
#include <Ethernet.h>
byte mac[] = {0x90, 0xA2, 0xDA, 0x0E, 0x94, 0xB7 };
IPAddress ip(172, 16, 7, 1);
IPAddress gateway(172, 16,7, 0);
IPAddress subnet(255, 255, 0, 0);
EthernetClient client;
EthernetServer iec104Server(2404);
int TypeQuerry, MessageLength;// тип APDU и длина посылки
uint8_t iec104ReciveArray[128];//буфер приема APDU
void setup()
{
  Ethernet.begin(mac, ip, gateway, subnet);
}
void loop()
{
  client = iec104Server.available();
  if(client.available())//клиент подсоединен
   {
     delay(100);
     int i = 0;
     while(client.available())//чтение байтов
     {
       iec104ReciveArray[i] = client.read();//записываем в буфер приема данные
       i++;
     }
    TypeQuerry= iec104ReciveArray[2];//определяем тип APDU
    switch(TypeQuerry)
  {
       case 07:// если пришел тип STARTDT
       iec104ReciveArray[0]=iec104ReciveArray[0];// START2 = 68h;
       iec104ReciveArray[1]=iec104ReciveArray[1];//длина APDU 
       iec104ReciveArray[2] = iec104ReciveArray[2]+4; //тип APDU
       iec104ReciveArray[3]=0;
       iec104ReciveArray[4]=0;
       iec104ReciveArray[5]=0;
       MessageLength = iec104ReciveArray[1]+2;//длина сообщения + 2 байта Start and Lenght APCI
       delay(100);
       client.write(iec104ReciveArray, MessageLength);//передача обратно
    break;
  }
 }
}


После обновления скетча наблюдаем следующий порядок обмена:



Установку соединения, запрос на передачу данных, подтверждение запроса и еще один новый пока неизвестный APDU формата I типа 1 C_IC_NA Act.

3. Запрос на общий опрос станции


Команда опроса C_IC ACT запрашивает полный объем или заданный определенный поднабор опрашиваемой информации на КП. Поднабор (группа) выбирается с помощью описателя опроса QOI.
Команда опроса станции требует от контролируемых станций передать актуальное состояние их информации, обычно передаваемой спорадически (причина передачи = 3), на контролирующую станцию с причинами передачи от <20> до <36>. Опрос станции используется для синхронизации информации о процессе на контролирующей станции и контролируемых станциях. Он также используется для обновления информации на контролирующей станции после процедуры инициализации или после того, как контролирующая станция обнаружит потерю канала (безуспешное повторение запроса канального уровня) и последующее восстановление его. Ответ на опрос станции должен включать объекты информации о процессе, которые запомнены на контролируемой станции. В ответ на опрос станции эти объекты информации передаются с идентификаторами типов <1>, <3>, <5>, <7>, <9>, <11>, <13>, <20> или <21> и могут также передаваться в других ASDU с идентификаторами типов от <1> до <14>, <20>, <21>, от <30> до <36> и с причинами передачи <1> — периодически/циклически, <2> — фоновое сканирование или <3> — спорадически.

Картинка



APDU <100> C_IC_NA_1 кроме блока APCI так же имеет блок ASDU (блок данных прикладного уровня), которые вместе формируют Протокольный Блок Данных Прикладного Уровня APDU.



Рассмотрим более подробно полученный APDU.

APCI:

  • В первом байте указан тип 0 означающий, что это команда опроса;
  • Во втором длина APDU 14 байт;

ASDU:

  • Первый байт в блоке ASDU определяет тип объекта информации, в данном случае <100> C_IC_NA_1 (общий опрос станции);
  • Второй структуру блока данных;
  • Третий причину передачи (CauseTx), значение шесть означает запрос на активацию;
  • Четвертый общий адрес стануии;
  • Пятый адрес контролируемой (slave) станции;
  • С шестого по восьмой адрес объекта информации равен нулю;
  • Девятый информационный байт — QOI — описатель запроса, имеющий следующие значения:

В ответ на <100> C_IC_NA_1 необходимо ответить подтверждением на запрос, передать объекты информации о процессе, которые запомнены на контролируемой станции и завершить активацию.
Для отправки подтверждения необходимо в ASDU <100> C_IC_NA_1 записать в байт указывающий на причину передачи (CauseTX) значение равное 7, для оправки завершения активации необходимо записать в байт указывающий на причину передачи (CauseTX) значение равное 10.

Скетч
      case 00://опрос станции 
      txcnt=txcnt+02;
      //Подтверждение общего опроса 
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=iec104ReciveArray[1];
      iec104ReciveArray[2]=lowByte(txcnt);//TX L
      iec104ReciveArray[3]=highByte(txcnt);//TX H
      iec104ReciveArray[4]=lowByte(rxcnt);//RX L
      iec104ReciveArray[5]=highByte(rxcnt);//RX H
      iec104ReciveArray[6]=100;//опрос станции
      iec104ReciveArray[7]=01;
      iec104ReciveArray[8]=7;//cause Actcon
      iec104ReciveArray[9]=00;//OA
      iec104ReciveArray[10]=01;//Addr
      iec104ReciveArray[11]=00;//Addr
      iec104ReciveArray[12]=00;//IOA
      iec104ReciveArray[13]=00;//IOA
      iec104ReciveArray[14]=00;//IOA
      iec104ReciveArray[15]=20;//IOA, QOI
      MessageLength = iec104ReciveArray[1]+2;
      delay(100);
      client.write(iec104ReciveArray, MessageLength); 
      txcnt=txcnt+2;//увеличение счетчика переданных пакетов
    //актуальные состояния информации контролирующей станции в ответ на общий опрос 
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=14;//длина  APDU=APCI(4)+ ASDU(10)
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=1;//type 1
      iec104ReciveArray[7]=01;//sq
      iec104ReciveArray[8]=20;//Cause Inrogen
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[0];//IOA
      iec104ReciveArray[13]=iecData[1];//IOA
      iec104ReciveArray[14]=0;//IOA
      iec104ReciveArray[15]=iecData[2];//value [DATA 1]
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength); 
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=14;
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=1;//type 1 bool
      iec104ReciveArray[7]=01;
      iec104ReciveArray[8]=20;//Cause Inrogen
      iec104ReciveArray[9]=00;
      iec104ReciveArray[10]=01;
      iec104ReciveArray[11]=00;
      iec104ReciveArray[12]=iecData[3];
      iec104ReciveArray[13]=iecData[4];
      iec104ReciveArray[14]=0;
      iec104ReciveArray[15]=iecData[5];
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength); 
      delay(5);
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=22; 
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=11;//type 11 int
      iec104ReciveArray[7]=02;//sq
      iec104ReciveArray[8]=20;//Cause Inrogen
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[6];//IOA
      iec104ReciveArray[13]=iecData[7];//IOA
      iec104ReciveArray[14]=0;//IOA
      iec104ReciveArray[15]=iecData[8];//value [DATA 1]
      iec104ReciveArray[16]=iecData[9];//value [DATA 1] 
      iec104ReciveArray[17]=iecData[10];//QDS 
      iec104ReciveArray[18]=iecData[11];//IOA
      iec104ReciveArray[19]=iecData[12];//OA
      iec104ReciveArray[20]=0;//IOA
      iec104ReciveArray[21]=iecData[13];//value  [DATA 2]
      iec104ReciveArray[22]=iecData[14];//value  [DATA 2]
      iec104ReciveArray[23]=iecData[15];//IOA QDS 
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      delay(5);
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=26; 
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=13;//type 13 Float
      iec104ReciveArray[7]=02;//sq
      iec104ReciveArray[8]=20;//Cause Inrogen
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[16];//IOA
      iec104ReciveArray[13]=iecData[17];//IOA
      iec104ReciveArray[14]=0;
      iec104ReciveArray[15]=iecData[18];//value [DATA 1]
      iec104ReciveArray[16]=iecData[19];//value [DATA 1]
      iec104ReciveArray[17]=iecData[20];//value [DATA 1]
      iec104ReciveArray[18]=iecData[21];//value [DATA 1]
      iec104ReciveArray[19]=iecData[22];//IOA QDS
      iec104ReciveArray[20]=iecData[23];//IOA
      iec104ReciveArray[21]=iecData[24];//IOA
      iec104ReciveArray[22]=0;//IOA
      iec104ReciveArray[23]=iecData[25];//value [DATA 2]
      iec104ReciveArray[24]=iecData[26];//value [DATA 2]
      iec104ReciveArray[25]=iecData[27];//value [DATA 2]
      iec104ReciveArray[26]=iecData[28];//value [DATA 2]
      iec104ReciveArray[27]=iecData[29];//IOA QDS 
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      txcnt=txcnt+2;
    //Завершение общего опроса
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=iec104ReciveArray[1];
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=100;//type
      iec104ReciveArray[7]=01;//sq
      iec104ReciveArray[8]=10;//cause AckTerm
      iec104ReciveArray[9]=00;
      iec104ReciveArray[10]=01;
      iec104ReciveArray[11]=00;
      iec104ReciveArray[12]=00;
      iec104ReciveArray[13]=00;
      iec104ReciveArray[14]=00;
      iec104ReciveArray[15]=20;
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      iec104ReciveArray[6]=00;//сброс для нормальног определения нового входящего пакета
    break;


После обновления скетча наблюдаем следующий порядок обмена:


Установку соединения, запрос на передачу данных, подтверждение, запрос на общий опрос станции от контролирующей станции, завершение инициализации, запрос на общий опрос станции в направление контролируемой станции, подтверждение общего опроса, пересылка значений всех доступных сигналов на контролирующей станции, завершение общего опроса и неизвестный APCI формата S.
Запрос опроса станции выдается в направлении контролируемой станции:
— если с контролируемой станции получен «КОНЕЦ ИНИЦИАЛИЗАЦИИ» или
— если центральная станция обнаруживает потерю канала (безуспешное повторение запроса канального уровня) и последующее восстановление его.

4. Подготовка и передача данных


APDU блок формата S, состоящий только из APCI предназначен для подтверждения принятого APDU I формата. Для S-формата 7 старших бит служебного поля байта 1 и байт 2 не задействованы, а байт 3 (7 старших бит) и байт 4 определяют текущий номер принятой посылки.



В данном случае блок S указывает на то, что контролирующая (master) станция готова к приему данных в течении определенного времени, не превышающего, тайм-аут t3 определенного на стороне контролирующей (master) станции. То есть контролирующая (master) станция говорит нам «я готова к приему данных!». Далее необходимо позаботиться о том какие данные передавать и откуда их брать.

Что можно передавать? Существует несколько видов информации определённых в МЭК 870-5- 104:

  • Контрольная;
  • Управляющая;
  • Параметры;
  • Передача файлов.

Картинка


В данном примере рассматривается передача контрольной информации на примере 1, 11 и 13 функций (одноэлементная, измерение масштабируемое, измерение короткий формат с плавающей запятой). Данные формируются рандомно. Также необходимо учитывать, что у каждого передаваемого сигнала имеется байт качества.



Простой алгоритм определения качества сигнала:

  • Если используется замещение действующего сигнала то выставляются флаги BL(блокировка) и SB(замещение);
  • Если значение сигнала не изменялось в течении контрольного промежутка времени то выставляется флаг NT(не актуальное);
  • Если имеется признак неработоспособности узла или устройства более нижнего уровня (датчик или прочее) то выставляется флаг IV(не достоверное значение).

Скетч
void SetQDS(int currvalue, int i,bool zam)//определение качества сигнала
{
if (zam==0)//замещение?
{
  if (currvalue==previusValue[i])//значение не изменялось?
    {
      previusValue[i]=currvalue;
      counter[i]+=1;
      if (counter[i]>=1000)
        {
          qds[i]=64;// NT 
          counter[i]=0;
        }
    }
    else
    {
        qds[i]=0;
        counter[i]=0;
        previusValue[i]=currvalue;
    }
}
else 
  {
    qds[i]=48;// SB, BL   
  }
}


Так же необходимо учесть, что для каждого сигнала имеется уникальный идентификатор IOA, в энергетике принято распределять эти адреса следующим образом:

  • ТС-начиная с 4096;
  • ТИ-начиная с 8192;
  • ТУ-начиная с 20480.

Для передачи значения сигналов в массив для отправки используется библиотека EEPROM:

Скетч
void EEPROM_float_write(int addr, float val,int IOA,int number,bool subs) // начальный адрес в EEPROM, значение сигнала, адрес сигнала, порядковый номер измеряемого сигнала, замещение
{  
  SetQDS(val,number, subs);//установка качества сигнала
  byte *x = (byte *)&val;//float -->byte
  byte *xxx = (byte *)&IOA;//запись адреса IOA
  for(int jj = 0; jj <2; jj++)
    {
      EEPROM.write(addr,xxx[jj]);//сохранение в EEPROM адреса блока данных в 2 байтах
      addr+=1;
    }
  for(byte i = 0; i < 4; i++) //запись формата float в 4 байтах
    {
      EEPROM.write(addr, x[i]); //запись формата float в 4 байтах
      addr+=1;
    }
  EEPROM.write(addr, qds[number]);//запись информации о качестве сигнала
  if (addr == EEPROM.length())
    {
      addr = 0;
    }
}


И так получив APDU подтверждение S или I формата от контролирующей станции можно начать передавать имеющиеся в распоряжении данные, не забывая увеличивать номер передаваемого кадра.


Скетч
#include <eeprom.h>
#include <Ethernet.h>
#include <eeprom.h>
byte mac[] = {0x90, 0xA2, 0xDA, 0x0E, 0x94, 0xB7 };
IPAddress ip(172, 16, 7, 1);
IPAddress gateway(172, 16,7, 0);
IPAddress subnet(255, 255, 0, 0);
EthernetClient client;
EthernetServer iec104Server(2404);
int TypeQuerry, MessageLength;
uint8_t iec104ReciveArray[128];
int counter[6];//порядковый номер сигнала
int qds[6];//порядковый номер для определения качества сигнала
int previusValue[6];//порядковый номер для определения статуса NT
word iecData[256];//буфер для хранения значений сигналов
int ASDU;//для определения типа входящего пакета
int txcnt, rxcnt;//счетчики переданных и принятых пакетов
void setup()
{
  //создание сервера 2404 порт
  Ethernet.begin(mac, ip, gateway, subnet);
  Serial.begin(9600);

 }
 void EEPROM_float_write(int addr, float val,int IOA,int number,bool zam) // запись в ЕЕПРОМ значения типа Float функция 13
{  
  SetQDS(val,number,zam);//качество сигнала  
  byte *x = (byte *)&val;
  byte *xxx = (byte *)&IOA;
  for(int jj = 0; jj <2; jj++)//запись адреса IOA в 2 байта
    {
      EEPROM.write(addr,xxx[jj]);
      addr+=1;  
    }
  for(byte i = 0; i < 4; i++)//запись формата float в 4 байта 
  {
      EEPROM.write(addr, x[i]);
      addr+=1;
  }
      EEPROM.write(addr, qds[number]);
     if (addr == EEPROM.length())
       {
          addr = 0;
       }
}
void EEPROM_byte_write(int addr, bool val,int IOA,int number,bool zam) // запись в ЕЕПРОМ значения типа Bool функция 1
{  
  SetQDS(val,number,zam);//качество сигнала  
  byte c=val+qds[number];
  byte *x = (byte *)&c;
  byte *xxxx = (byte *)&IOA;
  for(int jj = 0; jj <2; jj++) //запись адреса IOA в 2 байта
    { 
      EEPROM.write(addr,xxxx[jj]);
      addr+=1;  
    }
  for(byte i = 0; i < 1; i++) //запись формата bool + качество сигнала в 1 байт 
  {
  EEPROM.write(addr, x[i]);
  }
  if (addr == EEPROM.length()) {
    addr = 0;
  }
}
void EEPROM_int_write(int addr, int val, int IOA,int number,bool zam) // запись в ЕЕПРОМ значения типа Int функция 11
{ 
  SetQDS(val,number,zam); //качество сигнала 
  byte *x = (byte *)&val;
  byte *xx = (byte *)&IOA;
  for(int jj = 0; jj <2; jj++)//запись адреса IOA в 2 байта
    {
      EEPROM.write(addr,xx[jj]);
      addr+=1;  
      
    }
    for(byte i = 0; i < 2; i++)//запись формата int  в 2 байтa
  {
  EEPROM.write(addr, x[i]);
   addr+=1; 
  }
   EEPROM.write(addr, qds[number]);
     if (addr == EEPROM.length()) {
    addr = 0;
  }
}
//установка качества сигнала
void SetQDS(int currvalue, int i,bool zam)//текущее значение, порядковый номер измеряемого сигнала, замещение
{
if (zam==0)//замещение?
{
  if (currvalue==previusValue[i])//значение не изменялось?
    {
      previusValue[i]=currvalue;
      counter[i]+=1;
      if (counter[i]>=1000)
        {
          qds[i]=64;//установка флага NT
          counter[i]=0;
        }
      }
    else
      {
        qds[i]=0;
        counter[i]=0;
        previusValue[i]=currvalue;
      }
}
else 
  {
          qds[i]=48;//установка флагов замещение и блокировки
  }
}
void loop()
{ 
 
//Тестовые данные для 1,11,13 функций
//в формате: (адрес в EEPROM, значение сигнала, IOA адрес, качество сигнала)
   EEPROM_byte_write(0,0,4096,0,0);
   EEPROM_byte_write(3,random(0, 2),4097,1,1);  
   EEPROM_int_write(6,  67,8192,2,1);
   EEPROM_int_write(11, random(10, 20),8193,3,0);
   EEPROM_float_write(16, random(-1000, 2000),8194,4,1);
   EEPROM_float_write(23, 78.66f,8195,5,1);
   client = iec104Server.available();
   
if(client.available())
  {
    delay(100);
    int i = 0;
    while(client.available())
    {
    iec104ReciveArray[i] = client.read();//записываем в буфер приема данные
    i++;
    }
    ASDU=iec104ReciveArray[6];//пакет ASDU?
    switch (ASDU)
   {
    case 100://опрос тсанции
    TypeQuerry=iec104ReciveArray[2]-word(iec104ReciveArray[3],iec104ReciveArray[2]);
    rxcnt+=2;//увеличение счетчика принятых пакетов 
    break;
    case 0:
    TypeQuerry=iec104ReciveArray[2];//определяем тип посылки  
    break;
    default :
    TypeQuerry=iec104ReciveArray[2];
    break;
   }      
    for(byte z = 0; z <64; z++)   //чтение из еепром 
    {
      iecData[z]= EEPROM.read(z);
    }
 //Тип принятого APDU
  switch(TypeQuerry)
  {
    case 07://APDU STARTDT
       rxcnt=0;
       txcnt=0;
       iec104ReciveArray[0]=iec104ReciveArray[0];//кадр переменной длины, начинающийся байтом START2 = 68h;
       iec104ReciveArray[1]=iec104ReciveArray[1];//длина APDU 
       iec104ReciveArray[2]=11;//STARTDT con
       iec104ReciveArray[3]=0;
       iec104ReciveArray[4]=0;
       iec104ReciveArray[5]=0;
       MessageLength = iec104ReciveArray[1]+2;//длина пакета
       client.write(iec104ReciveArray, MessageLength);//отправка обратно
      //Инициализация
      iec104ReciveArray[0]=iec104ReciveArray[0];//кадр переменной длины, начинающийся байтом START2 = 68h;
      iec104ReciveArray[1]=14;//длина APDU
      iec104ReciveArray[2]=0;//тип, TX L
      iec104ReciveArray[3]=0;//TX H
      iec104ReciveArray[4]=0;//RX L
      iec104ReciveArray[5]=0;//RX H
      iec104ReciveArray[6]=70;//type End of Init
      iec104ReciveArray[7]=01;//sq
      iec104ReciveArray[8]=04;//cause Init
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=00;//IOA
      iec104ReciveArray[13]=00;//IOA
      iec104ReciveArray[14]=00;//IOA
      iec104ReciveArray[15]=129;//IOA, COI
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
     //Общий опрос в направление контролируемой станции 
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=14;//длина APDU
      iec104ReciveArray[2]=2;//TX L
      iec104ReciveArray[3]=0;//TX H
      iec104ReciveArray[4]=0;//RX L
      iec104ReciveArray[5]=0;//RX H
      iec104ReciveArray[6]=100;// опрос станции
      iec104ReciveArray[7]=01;
      iec104ReciveArray[8]=6;//cause Act
      iec104ReciveArray[9]=00;
      iec104ReciveArray[10]=01;
      iec104ReciveArray[11]=00;
      iec104ReciveArray[12]=00;
      iec104ReciveArray[13]=00;
      iec104ReciveArray[14]=00;
      iec104ReciveArray[15]=20;//IOA, QOI общий
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength); 
      txcnt=txcnt+02;
      break;
      case 00://опрос станции 
      txcnt=txcnt+02;
      //Подтверждение общего опроса 
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=iec104ReciveArray[1];
      iec104ReciveArray[2]=lowByte(txcnt);//TX L
      iec104ReciveArray[3]=highByte(txcnt);//TX H
      iec104ReciveArray[4]=lowByte(rxcnt);//RX L
      iec104ReciveArray[5]=highByte(rxcnt);//RX H
      iec104ReciveArray[6]=100;//опрос станции
      iec104ReciveArray[7]=01;
      iec104ReciveArray[8]=7;//cause Actcon
      iec104ReciveArray[9]=00;//OA
      iec104ReciveArray[10]=01;//Addr
      iec104ReciveArray[11]=00;//Addr
      iec104ReciveArray[12]=00;//IOA
      iec104ReciveArray[13]=00;//IOA
      iec104ReciveArray[14]=00;//IOA
      iec104ReciveArray[15]=20;//IOA, QOI
      MessageLength = iec104ReciveArray[1]+2;
      delay(100);
      client.write(iec104ReciveArray, MessageLength); 
      txcnt=txcnt+2;//увеличение счетчика переданных пакетов
    //актуальные состояния информации контролирующей станции в ответ на общий опрос 
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=14;//длина  APDU=APCI(4)+ ASDU(10)
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=1;//type 1
      iec104ReciveArray[7]=01;//sq
      iec104ReciveArray[8]=20;//Inrogen
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[0];//IOA
      iec104ReciveArray[13]=iecData[1];//IOA
      iec104ReciveArray[14]=0;//IOA
      iec104ReciveArray[15]=iecData[2];//value [DATA 1]
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength); 
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=14;
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=1;
      iec104ReciveArray[7]=01;
      iec104ReciveArray[8]=20;
      iec104ReciveArray[9]=00;
      iec104ReciveArray[10]=01;
      iec104ReciveArray[11]=00;
      iec104ReciveArray[12]=iecData[3];
      iec104ReciveArray[13]=iecData[4];
      iec104ReciveArray[14]=0;
      iec104ReciveArray[15]=iecData[5];
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength); 
      delay(5);
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=22; 
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=11;//type 11
      iec104ReciveArray[7]=02;//sq
      iec104ReciveArray[8]=20;//cause
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[6];//IOA
      iec104ReciveArray[13]=iecData[7];//IOA
      iec104ReciveArray[14]=0;//IOA
      iec104ReciveArray[15]=iecData[8];//value [DATA 1]
      iec104ReciveArray[16]=iecData[9];//value [DATA 1] 
      iec104ReciveArray[17]=iecData[10];//QDS 
      iec104ReciveArray[18]=iecData[11];//IOA
      iec104ReciveArray[19]=iecData[12];//OA
      iec104ReciveArray[20]=0;//IOA
      iec104ReciveArray[21]=iecData[13];//value  [DATA 2]
      iec104ReciveArray[22]=iecData[14];//value  [DATA 2]
      iec104ReciveArray[23]=iecData[15];//IOA QDS 
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      delay(5);
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=26; 
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=13;//type 13
      iec104ReciveArray[7]=02;//sq
      iec104ReciveArray[8]=20;//cause
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[16];//IOA
      iec104ReciveArray[13]=iecData[17];//IOA
      iec104ReciveArray[14]=0;
      iec104ReciveArray[15]=iecData[18];//value [DATA 1]
      iec104ReciveArray[16]=iecData[19];//value [DATA 1]
      iec104ReciveArray[17]=iecData[20];//value [DATA 1]
      iec104ReciveArray[18]=iecData[21];//value [DATA 1]
      iec104ReciveArray[19]=iecData[22];//IOA QDS
      iec104ReciveArray[20]=iecData[23];//IOA
      iec104ReciveArray[21]=iecData[24];//IOA
      iec104ReciveArray[22]=0;//IOA
      iec104ReciveArray[23]=iecData[25];//value [DATA 2]
      iec104ReciveArray[24]=iecData[26];//value [DATA 2]
      iec104ReciveArray[25]=iecData[27];//value [DATA 2]
      iec104ReciveArray[26]=iecData[28];//value [DATA 2]
      iec104ReciveArray[27]=iecData[29];//IOA QDS 
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      txcnt=txcnt+2;
    //Завершение общего опроса
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=iec104ReciveArray[1];
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=100;//type
      iec104ReciveArray[7]=01;//sq
      iec104ReciveArray[8]=10;//cause AckTerm
      iec104ReciveArray[9]=00;
      iec104ReciveArray[10]=01;
      iec104ReciveArray[11]=00;
      iec104ReciveArray[12]=00;
      iec104ReciveArray[13]=00;
      iec104ReciveArray[14]=00;
      iec104ReciveArray[15]=20;
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      iec104ReciveArray[6]=00;//сброс для нормальног определения нового входящего пакета
    break;
   //APDU S
    case 01:
      txcnt=word(iec104ReciveArray[5],iec104ReciveArray[4]);
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=14;
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=1;//type 1
      iec104ReciveArray[7]=01;//sq
      iec104ReciveArray[8]=01;//cause Cycl
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[0];//IOA
      iec104ReciveArray[13]=iecData[1];//IOA
      iec104ReciveArray[14]=0;//IOA
      iec104ReciveArray[15]=iecData[2];//value [DATA 1]
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength); 
      delay(5);
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=14;
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=1;//type 1 Bool
      iec104ReciveArray[7]=01;//sq
      iec104ReciveArray[8]=01;//cause Cycl
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[3];//IOA
      iec104ReciveArray[13]=iecData[4];//IOA
      iec104ReciveArray[14]=0;//IOA
      iec104ReciveArray[15]=iecData[5];//value [DATA 1]
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength); 
      delay(5);
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=22; 
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=11;//type 11 Int
      iec104ReciveArray[7]=02;//sq
      iec104ReciveArray[8]=01;//cause Cycl
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[6];//IOA
      iec104ReciveArray[13]=iecData[7];//IOA
      iec104ReciveArray[14]=0;//IOA
      iec104ReciveArray[15]=iecData[8];//value  [DATA 1]
      iec104ReciveArray[16]=iecData[9];//value  [DATA 1]
      iec104ReciveArray[17]=iecData[10];//QDS 
      iec104ReciveArray[18]=iecData[11];//IOA
      iec104ReciveArray[19]=iecData[12];//OA
      iec104ReciveArray[20]=0;//IOA
      iec104ReciveArray[21]=iecData[13];//value  [DATA 2]
      iec104ReciveArray[22]=iecData[14];//value  [DATA 2]
      iec104ReciveArray[23]=iecData[15];//IOA QDS 
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      delay(5);
      txcnt=txcnt+2;
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=26;
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=13;//type 13 Float
      iec104ReciveArray[7]=02;//sq
      iec104ReciveArray[8]=01;//cause Cycl
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[16];//IOA
      iec104ReciveArray[13]=iecData[17];//IOA
      iec104ReciveArray[14]=0;
      iec104ReciveArray[15]=iecData[18];//value [DATA 1]
      iec104ReciveArray[16]=iecData[19];//value  [DATA 1]
      iec104ReciveArray[17]=iecData[20];//value [DATA 1]
      iec104ReciveArray[18]=iecData[21];//value  [DATA 1]
      iec104ReciveArray[19]=iecData[22];//IOA QDS
      iec104ReciveArray[20]=iecData[23];//IOA
      iec104ReciveArray[21]=iecData[24];//IOA
      iec104ReciveArray[22]=0;//IOA
      iec104ReciveArray[23]=iecData[25];//value [DATA 2]
      iec104ReciveArray[24]=iecData[26];//value [DATA 2]
      iec104ReciveArray[25]=iecData[27];//value [DATA 2]
      iec104ReciveArray[26]=iecData[28];//value [DATA 2]
      iec104ReciveArray[27]=iecData[29];//IOA QDS 
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
      txcnt=txcnt;
    break;
    case 67:
    //TESTFR 
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=iec104ReciveArray[1];
      iec104ReciveArray[2] =131; //TESTFR con
      iec104ReciveArray[3] =0;
      iec104ReciveArray[4] =0;
      iec104ReciveArray[5] =0;
      MessageLength = iec104ReciveArray[1]+2;
      delay(10);
      client.write(iec104ReciveArray, MessageLength);
      iec104ReciveArray[0]=iec104ReciveArray[0];
      iec104ReciveArray[1]=26;//длина  APDU
      iec104ReciveArray[2]=lowByte(txcnt);
      iec104ReciveArray[3]=highByte(txcnt);
      iec104ReciveArray[4]=lowByte(rxcnt);
      iec104ReciveArray[5]=highByte(rxcnt);
      iec104ReciveArray[6]=13;//type 13
      iec104ReciveArray[7]=02;//sq
      iec104ReciveArray[8]=03;//spont
      iec104ReciveArray[9]=00;//AO
      iec104ReciveArray[10]=01;//Adress
      iec104ReciveArray[11]=00;//Adress
      iec104ReciveArray[12]=iecData[16];//IOA
      iec104ReciveArray[13]=iecData[17];//IOA
      iec104ReciveArray[14]=0;
      iec104ReciveArray[15]=iecData[18];//value [DATA 1]
      iec104ReciveArray[16]=iecData[19];//value  [DATA 1]
      iec104ReciveArray[17]=iecData[20];//value [DATA 1]
      iec104ReciveArray[18]=iecData[21];//value  [DATA 1]
      iec104ReciveArray[19]=iecData[22];//IOA QDS
      iec104ReciveArray[20]=iecData[23];//IOA
      iec104ReciveArray[21]=iecData[24];//IOA
      iec104ReciveArray[22]=0;//IOA
      iec104ReciveArray[23]=iecData[25];
      iec104ReciveArray[24]=iecData[26];
      iec104ReciveArray[25]=iecData[27];
      iec104ReciveArray[26]=iecData[28];
      iec104ReciveArray[27]=iecData[29];//IOA QDS 
      MessageLength = iec104ReciveArray[1]+2;
      client.write(iec104ReciveArray, MessageLength);
     break;
  }
  }
 }



Загрузив скетч в Wireshark наблюдаем, что наконец-то началась передача данных.

Далее привожу описание структуры ASDU <100>M_SP_NA_1 одноэлементная индикация.



TypeId — вид информации.
SQ — классификатора переменной структуры.

Предусматриваются две структуры блоков данных:

1. Блок, содержащий i объектов информации, каждый из которых содержит по одному элементу информации (или по одной комбинации элементов); старший бит классификатора переменной структуры SQ (single/sequence) равен 0, остальные 7 битов задают число i.

2. Блок, содержащий один объект информации, который содержит j элементов либо одинаковых комбинаций элементов информации; старший бит (27 = 80h) классификатора SQ равен 1, остальные 7 битов задают число j.

Картинка


CauseTx — причина передачи.

Картинка


Addr — адрес слэйва (указывается при конфигурировании мастера).
IOA — адрес объекта информации, по этому адресу контролирующая станция будет привязывать свой тэг
SIQ — показатель качества передаваемого сигнала.

Структура ASDU блока функции <11>M_ME_NB_1:

Wireshark


В ответ на полученные данные master будет отправлять блоки формата S и процесс зациклится до тех пор пока контролируемое(slave) устройство не перестанет передавать кадры.

5. Процедуры тестирования


Процедуры тестирования применяются с целью контроля за работоспособностью транспортных соединений. Процедура выполняется независимо от «активности» IP-соединения, если в течение контрольного времени t3 не было принято ни одного кадра (I, U, S). Время t3 является предметом согласования и называется «Тайм-аут для посылки блоков тестирования в случае долгого простоя». Процедура тестирования реализуется путем посылки тестового APDU (TESTFR =act), которое подтверждается принимаемой станцией с помощью APDU (TESTFR =con).

Картинка


Wireshark


Если от контролирующей (master) станции придет APDU у которого в байте отвечающего за тип APDU значение равно шестидесяти сети (TESTFR) это говорит о том, что в течении времени t3 от контролируемой станции не было принято ни одного кадра (I, U, S), и если в течении времени t1 не ответить подтверждением то соединение будет разорвано.

Скетч
case 67:
      iec104ReciveArray[0]=iec104ReciveArray[0];//кадр переменной длины, начинающийся байтом START2 = 68h;
      iec104ReciveArray[1]=iec104ReciveArray[1];//длина APDU LENGHT
      iec104ReciveArray[2] = 131; //TESTDT con
      iec104ReciveArray[3] =0;
      iec104ReciveArray[4] =0;
      iec104ReciveArray[5] =0;
      MessageLength = iec104ReciveArray[1]+2;//определение длины сообщения + 2 байта Start68H and Lenght
      delay(10);
      client.write(iec104ReciveArray, MessageLength);


Wireshark


На этом всё, если кому-нибудь интересно то в следующей статье я рассмотрю протокол МЭК 670-5-104 со стороны контролирующей (master) станции на примере Arduino.
Tags:
Hubs:
+13
Comments 8
Comments Comments 8

Articles