Сбор показаний с счетчиков "Меркурий 200-203"

Однажды возникла задача — научится считывать показания со счетчиков «Меркурий 200-203-206» ...

Первым делом оказалось, что счетчики золотые, на сайте кроме паспортов и примитивного описания ничего более нет.
Это и понятно, НПК-Инкотекс взял за основу пакет Modbus RTU (убрал из него все логичное и полезное — решив использовать фиксированные поля, без кода ошибок и т.д.,) расширив поле ID до 4-х байт, оставшееся было наречено «собственным протоколом». Что позволяет продавать (в единственном лице) конвертеры из «собственного протокола» в нормальный Modbus.

Первым делом был скачан с офсайта конфигуратор:


Подключен сам счетчик (USB-485), и подсмотрены сами пакеты:


Оказалось, что конфигуратор совершал 7 запросов, а именно:
— 0x28 (Чтение идентификационных данных счетчика)
— 0x2F (Чтение псевдосерийного номера)
— 0x29 (Чтение напряжения на литиевой батарее)
— 0x2C (Чтение времени последнего включения напряжения)
— 0x2B (Чтение времени последнего отключения напряжения)
и неизвестные 0x66-65
Далее было раздобыто описание «протокола» и стало совсем все понятно.
В запросе ID это первые четыре байта (0x000D1F или 859973), байт команды и два байта CRC.
В ответе так же ID, команда и набор параметров (длина представляет магическую цифру, определяется самим параметром, может быть от 0 до 17 байт).
С пакетом вроде все понятно, осталось написать программу )

Собрал платку на STM32F107VC (CAN с USB был желателен), 4-е MAX485 на столько же USART-UART портов, + выводы DE/RE
Написал структуры, простите за названия:

/* по порядку портов */
#define MOD0		0x00
#define MOD1		0x01
#define MOD2		0x02
#define MOD3		0x03
#define CAN		0x04
/* биты окончания отправки */
#define END_MOD0	0x01
#define END_MOD1	0x02
#define END_MOD2	0x04
#define END_MOD3	0x08
#define END_CAN		0x10

/* главная структура, логика опроса */
typedef struct sInterface
{
	uint8_t ModEnab;        // флаг начала цикла опроса
	uint8_t ModDev0;	// сколько всего устройств опрашивается, линия 1    
	uint8_t ModDev1;        // сколько всего устройств опрашивается, линия 2    
	uint8_t ModDev2;        // сколько всего устройств опрашивается, линия 3   
	uint8_t ModDev3;        // сколько всего устройств опрашивается, линия 4  
	uint8_t CANDev;         // CAN, на перспективу )
	uint8_t CurDevMod0;     // текущий номер устройства, линия 1    
	uint8_t CurDevMod1;     // текущий номер устройства, линия 1      
	uint8_t CurDevMod2;     // текущий номер устройства, линия 1      
	uint8_t CurDevMod3;     // текущий номер устройства, линия 1      
	uint8_t CurRegDevMod0;  // текущий номер регистра, линия 1     
	uint8_t CurRegDevMod1;  // текущий номер регистра, линия 1       
	uint8_t CurRegDevMod2;  // текущий номер регистра, линия 1       
	uint8_t CurRegDevMod3;  // текущий номер регистра, линия 1       
	uint8_t AllRegDev0;     // всего параметров (за сколько раз опрашивается одно устройство), линия 1        
	uint8_t AllRegDev1;     // всего параметров (за сколько раз опрашивается одно устройство), линия 1        
	uint8_t AllRegDev2;     // всего параметров (за сколько раз опрашивается одно устройство), линия 1        
	uint8_t AllRegDev3;     // всего параметров (за сколько раз опрашивается одно устройство), линия 1        
	uint8_t ModEndFlag;     // флаг окончания опроса
}sInterface;

/* указываются ID и тип счетчика */
typedef struct
{
	uint32_t ID;
	uint8_t  Type;
}sDeviceModbus;

/* для отправки-приема данных */
typedef struct
{
	uint8_t EnabFlag;
	uint8_t TX_Buf[30];
	uint8_t RX_Buf[110];
	uint8_t RX_cou_byte;
	uint8_t TX_cou_byte;
	uint8_t RxBusy;
	uint32_t ID;
	uint8_t SelBaud;
	uint16_t BaudRate[4];
	uint8_t LengPar;
	uint8_t TypeDevice;
	uint8_t Option;
} sModbus_Struct;

/* линии ведь 4 */
extern sModbus_Struct ModDataStruct[4];
extern sDeviceModbus DevModLin0[240];
extern sDeviceModbus DevModLin1[240];
extern sDeviceModbus DevModLin2[240];
extern sDeviceModbus DevModLin3[240];
extern sInterface Interface;


Далее описал сам счетчик:

    /* просто идентификатор */
    #define DEVICE_MERC_200	70
    
    /* полученные данные дальше отправляются на сервер, их там надо еще разобрать */
    /* не пожалел 5 байт, это должно облегчить задачу */
    const char Dev_string_ME200[]    =	{"ME200"};
    const char Dev_string_ME203[]    =	{"ME203"};

    /* сами параметры, char-ы так же вставляются в пакет, помогая распознавать параметр */
    #define 	MERC200_LENG	10
    const char DevTable_Merc200[MERC200_LENG][3]=
         {
    	{'G','A', 0x20},
	{'I','C', 0x21},
	{'C','W', 0x26},
	{'V','A', 0x27},
	{'U','D', 0x28},
	{'L','U', 0x2B},
	{'L','Y', 0x2C},
	{'H','T', 0x2E},
	{'P','I', 0x2F},
	{'M','S', 0x32}
	};

    #define 	MERC203_LENG	10
    const char DevTable_Merc203[MERC203_LENG][3]=
         {
	{'G','A', 0x21},
	{'I','C', 0x21},
	{'C','W', 0x26},
	{'V','A', 0x27},
	{'U','D', 0x28},
	{'L','U', 0x2B},
	{'L','Y', 0x2C},
	{'H','T', 0x2E},
	{'P','I', 0x2F},
	{'M','S', 0x32}
	};


/* осталось самое сложное*/
void vModBusControl(void *pvParameters)
{
    uint8_t i;
    uint8_t Line;
    sDeviceModbus * PDevModLine;
    uint8_t * PModDev;
    uint8_t *PcurRegDevMod;
    uint8_t *PcurDevMod;
    uint8_t * PAllRegDev;
    uint8_t PEnd_MOD;

    for( ;; )
    {
	
        if(Interface.ModEnab == ON)
        {
	    Line = NULL;
	    /* until cycle is completed*/
	    while(Interface.ModEndFlag != (END_MOD0 | END_MOD1 | END_MOD2 | END_MOD3 | END_CAN))
	    {
                /* в моем случае было важно не допустиить превышения размера пакета */
                /* тогда можно отправлять пакет в середине цикла */
	        /* if size net Pack > MAX_SIZE_NET_PACK	*/
	        if(NetParam.NetBufCounter > MAX_SIZE_NET_PACK)
		{
		    /* then pause request interface, and send network packet */
		    StructConveer.Net |= CONV_NET_SEND;
		    while(StructConveer.Net &  CONV_NET_SEND){ vTaskDelay(10); };
		}
		ClearModTXRXbuf();
			
getLine:			
		/* Get Line */
		switch(Line)
		{
		    case MOD0: 
		    PDevModLine = &DevModLin0[Interface.CurDevMod0];
		    PModDev = &Interface.ModDev0;
		    PcurRegDevMod = &Interface.CurRegDevMod0;
		    PAllRegDev = &Interface.AllRegDev0;
		    PcurDevMod = &Interface.CurDevMod0;
		    PEnd_MOD = END_MOD0;
		break;
		
                case MOD1: 
		    PDevModLine = &DevModLin1[Interface.CurDevMod1];
		    PModDev = &Interface.ModDev1;
		    PcurRegDevMod = &Interface.CurRegDevMod1;
		    PAllRegDev = &Interface.AllRegDev1;
		    PcurDevMod = &Interface.CurDevMod1;
		    PEnd_MOD = END_MOD1;
		break;
		
		case MOD2: 
                    PDevModLine = &DevModLin2[Interface.CurDevMod2];
	    	    PModDev = &Interface.ModDev2;
		    PcurRegDevMod = &Interface.CurRegDevMod2;
		    PAllRegDev = &Interface.AllRegDev2;
		    PcurDevMod = &Interface.CurDevMod2;
		    PEnd_MOD = END_MOD2;
		break;
		
                case MOD3: 
		    PDevModLine = &DevModLin3[Interface.CurDevMod3];
		    PModDev = &Interface.ModDev3;
		    PcurRegDevMod = &Interface.CurRegDevMod3;
		    PAllRegDev = &Interface.AllRegDev3;
		    PcurDevMod = &Interface.CurDevMod3;
		    PEnd_MOD = END_MOD3;
		break;
		
        	case CAN:	 
		    PEnd_MOD = END_CAN;
		break;
		}
			
		/* Line */
		/* if Line is use */
		if(*PModDev != 0)
		{
		    if(PDevModLine->ID != 0)
		    {
			if(*PcurDevMod < *PModDev)
			{
		        	ReadyPackModbus((sModbus_Struct*)&ModDataStruct[Line], PDevModLine, &ModDataStruct[Line].LengPar, *PcurRegDevMod, PAllRegDev);
			}
		    }
				
		/* one approach */
		if(*PAllRegDev == 1)
		{
		    if(*PcurDevMod >= *PModDev)
		    {
			Interface.ModEndFlag |= PEnd_MOD;
		}
		else	
		{
		    (*PcurDevMod) ++;
        	}
	
	    }
            /* some approaches reading */
	    else
	    {
	        /* Mercurii & misk */
		if(*PcurRegDevMod >= *PAllRegDev)
		{
		    *PcurRegDevMod = 0;
		    if(*PcurDevMod >= *PModDev)
		    {
		        Interface.ModEndFlag |= PEnd_MOD;
		    }
		    else	
		    {
		        (*PcurDevMod)++;
		    }	
		}
		else
		{
		    (*PcurRegDevMod)++;
		}
	    }
	}
	else
	{
	    Interface.ModEndFlag |= PEnd_MOD;
	}
	i= NULL;
	/* start send request */
	if((ModDataStruct[0].LengPar != 0)&&(!(Interface.ModEndFlag & END_MOD0)))
	{
	    i |= 0x01;
	}
	if((ModDataStruct[1].LengPar != 0)&&(!(Interface.ModEndFlag & END_MOD1)))
	{
	    i |= 0x02;
	}
	if((ModDataStruct[2].LengPar != 0)&&(!(Interface.ModEndFlag & END_MOD2)))
	{
	    i |= 0x04;
	}
	if((ModDataStruct[3].LengPar != 0)&&(!(Interface.ModEndFlag & END_MOD3)))
	{
	    i |= 0x08;
	}
	if(Line != CAN)
	{
            /* GOTO - а-та-та*/
	    Line ++;
	    goto getLine;
	}
			
	Modbus_Transmit_Select(i);
	vTaskDelay(200);
	Line = NULL;
			
        if(ModDataStruct[0].RX_cou_byte != 0)
	{
	    InsertingRetDatatoPacket((sModbus_Struct*)&ModDataStruct[0], 0);
	    ModDataStruct[0].RX_cou_byte = 0;
	}
	if(ModDataStruct[1].RX_cou_byte != 0)
	{
	    InsertingRetDatatoPacket((sModbus_Struct*)&ModDataStruct[1], 1);
	    ModDataStruct[1].RX_cou_byte = 0;
	}
	if(ModDataStruct[2].RX_cou_byte != 0)
	{
	    ModDataStruct[2].RX_cou_byte = 0;
	}
	if(ModDataStruct[3].RX_cou_byte != 0)
	{
	    ModDataStruct[3].RX_cou_byte = 0;
	}
    }
    
        /* здесь можно отправить собравшийся пакет */		
        ClearModTXRXbuf();
	/* очистка всего */
        Interface.CurDevMod0 =0;
        Interface.CurDevMod1 =0;
        Interface.CurDevMod2 =0;
        Interface.CurDevMod3 =0;
        Interface.CurRegDevMod0 =0;
        Interface.CurRegDevMod1 =0;
        Interface.CurRegDevMod2 =0;
        Interface.CurRegDevMod3 =0;
        Interface.ModEnab =0;
        Interface.ModEndFlag = NULL;
        InstallAlarm();
    }
		
    vTaskDelay(200);	
 }
}

для работы с USART DMA пока не стал писать, все в прерывании:

/* линия 1, остальные так же */
 void MODBUS_1_USART_IntHand(void)
 {
    if(MODBUS_1_USART->SR & USART_SR_RXNE)
    {		
        MODBUS_1_USART->SR &=~ USART_SR_RXNE;
        ModDataStruct[0].RX_Buf[ModDataStruct[0].RX_cou_byte] = MODBUS_1_USART->DR;
        ModDataStruct[0].RX_cou_byte ++;
        ModDataStruct[0].RxBusy = 1;
    }
    if(MODBUS_1_USART->SR & USART_SR_TC)
    {
        MODBUS_1_USART->SR &=~ USART_SR_TC;
        if(ModDataStruct[0].TX_cou_byte < ModDataStruct[0].LengPar)
        {
           	MODBUS_1_USART->DR =  ModDataStruct[0].TX_Buf[ModDataStruct[0].TX_cou_byte];
        	ModDataStruct[0].TX_cou_byte++;
	}
	else
	{
	    Modbus_Receive_Select(END_MOD0);
	}
    }
}

Так готовится запрос:

void ReadyPackModbus(sModbus_Struct *buf, sDeviceModbus * insert, uint8_t *len, char Parameter, uint8_t * AllRegDev)
{
    uint8_t i=0;
    uint16_t crc;
    buf->ID = insert->ID;
	
    switch(insert->Type)
    {
        case DEVICE_MERC_200:
            buf->TypeDevice 	= DEVICE_MERC_200;
	    buf->TX_Buf[i++]	= ((insert->ID & 0xFF000000)>>24);
	    buf->TX_Buf[i++]	= ((insert->ID & 0x00FF0000)>>16);
	    buf->TX_Buf[i++]	= ((insert->ID & 0x0000FF00)>>8);
	    buf->TX_Buf[i++]	= insert->ID & 0x000000FF;
	    buf->TX_Buf[i++]	= DevTable_Merc200[Parameter][2];
	    buf->Option 		= Parameter;
	    *AllRegDev = MERC200_LENG;
	    *len = 5;
	    break;
	}
	crc	= getCRC((uint8_t*)&buf->TX_Buf[0], *len);
	buf->TX_Buf[i++] = crc & 0x00FF;
	buf->TX_Buf[i++] = (crc & 0xFF00)>>8;
	*len = i;
}

А так «набивается» пакет:

void InsertingRetDatatoPacket(sModbus_Struct* Data, uint8_t Line)
{
    uint8_t i, i2;
    uint8_t Len;
    char TypeDevString[5];
    char TempString[10];
    switch(Data->TypeDevice)
    {
        case DEVICE_MERC_200: 
       	    /* 18 байт, с запасом для всех параметров */
            Len = 9;
	    strcpy((char*)&TypeDevString, (char*)&Dev_string_ME200);
        break;
    }
    /* Number Interface Line */
    ConvIntToAscii((uint8_t)Line, CAPT_BYTE1, (char*)&TempString, 0);
    NetParam.NetBuf[NetParam.NetBufCounter++] = '-';
    NetParam.NetBuf[NetParam.NetBufCounter++] = TempString[0];
    NetParam.NetBuf[NetParam.NetBufCounter++] = TempString[1];
    /* Type Device */			
    NetParam.NetBuf[NetParam.NetBufCounter++] = '#';
    NetParam.NetBuf[NetParam.NetBufCounter++] = TypeDevString[0];
    NetParam.NetBuf[NetParam.NetBufCounter++] = TypeDevString[1];
    NetParam.NetBuf[NetParam.NetBufCounter++] = TypeDevString[2];
    NetParam.NetBuf[NetParam.NetBufCounter++] = TypeDevString[3];
    NetParam.NetBuf[NetParam.NetBufCounter++] = TypeDevString[4];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = '#';
    /* ID Device (4 bytes) */			
    ConvIntToAscii((uint32_t)Data->ID, CAPT_BYTE4, (char*)&TempString, 0);
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[0];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[1];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[2];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[3];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[4];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[5];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[6];
    NetParam.NetBuf[NetParam.NetBufCounter ++] = TempString[7];
    NetParam.NetBuf[NetParam.NetBufCounter++]	 = '-';	
    
    i2 = 3;
    switch(Data->TypeDevice)
    {
    	case DEVICE_MERC_200 : 
    	    NetParam.NetBuf[NetParam.NetBufCounter++] = DevTable_Merc200[Data->Option][0];
	    NetParam.NetBuf[NetParam.NetBufCounter++] = DevTable_Merc200[Data->Option][1];
	    i2 = 0;
	break;
	case DEVICE_MERC_203 : break;
    }
    for(i=0;i<Len*2;i++)
    {
    	ConvIntToAscii(Data->RX_Buf[i2], CAPT_BYTE1, (char*)&TempString, 0);
    	NetParam.NetBuf[NetParam.NetBufCounter++] = TempString[0];
    	NetParam.NetBuf[NetParam.NetBufCounter++] = TempString[1];
    	i2 ++;
    }
}


В таком виде пакет отсылается на сервер:
  • +3
  • 02 сентября 2014, 17:09
  • khomin
  • 1
Файлы в топике: Меркурий.zip

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

RSS свернуть / развернуть
Описание на протоколы прикрепил
0
Первым делом был скачан с офсайта конфигуратор
А ссылочку не подскажешь?
Алсо, где ты нашел опиание протокола?
0
  • avatar
  • Vga
  • 02 сентября 2014, 17:32
пусть хоть здесь будет, в свободном доступе его не достать
0
www.incotexcom.ru/all_prog.htm
описание… просто нашел )
0
Что позволяет продавать (в единственном лице) конвертеры из «собственного протокола» в нормальный Modbus.
Справедливости ради, Меркурий продаёт универсальные адаптеры USB — RS-485 (Меркурий 221). Так же как и не скрывает, что счётчики работают и с другими адаптерами 485й шины. Вот счётчики с CAN-шиной наоборот, к CAN-шине отношения не имеют. Там, по сути, тот же RS-485, только уровнями сигнала отличаются, на практике достаточно поменять проводки местами и CAN-счётчик можно опрашивать по RS-485 сети, дома так и сделал. А вот протокол у них свой, но то что за основу взят модбас — очень удобно, к счётчикам на одну линию можно вешать другие модбас-устройства, не опасаясь конфликтов. Я, правда, с трехфазными Меркурий 230 имел дело. Классная штука. Считает большое количество параметров, в т.ч. активную/реактивную/полную/коэффициент мощности по каждой фазе, обновляя раз в секунду. Дома такой использую в качестве трехканального однофазного счетчика, можно смотреть потребление разных приборов.
Протокол в своё время выпросил у разработчика, скинули без особых проблем, только поинтересовались, нафига он мне.
0
  • avatar
  • ACE
  • 02 сентября 2014, 20:43
Считает большое количество параметров, в т.ч. активную/реактивную/полную/коэффициент мощности по каждой фазе, обновляя раз в секунду.
Прикольно. Жаль нельзя к своему подключиться и мониторить все это)
0
  • avatar
  • Vga
  • 03 сентября 2014, 06:11
У них есть ещё фишка передачи данных по сети питания.
Пытались использовать их счётчики для центролизованного учёта потребления узлов связи через с2000-интернет. Но практика показала что это бессмысленно. Поскольку все кто потом должен проверять показания замеряли расход за месяц умножали его на количество узлов и и месяцев в году и так прописывали в контракте с электросетевой компанией.

В. Общем такие штуки наверно удобны в для жеков в новых томах. А в свЗи оказались лишними.
+1
Еще у них похоже проблемы в понимании протоколов, например в описании на «Меркурий 230-AR» интерфейс указан — «CAN», в каком месте он «CAN» мне не понятно )
В добавок пакет не совместим с другими счетчиками, если собрать линию из 200-203-206 и 230, работать оно 100% не будет (проверено), ровно как если использовать 200 и другие устройства (с интерфейсом Modbus), ибо пакет не стандартный, косяки и проблемы обязательно будут, чего в коммерческом учете не хотелось бы )
0
Была у меня раньше задумка такая же, в итоге купил RPi, пол вечера для написания скрипта на питоне и до сих пор все работает
0
Добрый день!

Правильно ли я догадываюсь, что

а) Речь идет об обычном бытовом электросчетчике, который обслуживает, в том числе, и мою квартиру.
Вот таком:


б) У него есть незапломбированный и незапаролированный на уровне софта разъем через который к нему можно подключиться и считать как минимум показания, а как максимум — прочие параметры — текущий ток, мощность и т.п.
0
1) Да, он.
2) Нет, интерфейс опломбирован.
0
  • avatar
  • Vga
  • 12 сентября 2014, 19:28
Бли-и-и-ин… А уж обрадовался — счас замонстрячу онлайн монитор энергопотребления… нет в мире совершенства…
0
поставьте второй счетчик )
в разрыв между первым и проводкой )
И можно будет мониторить )
Потребляют они вроде немного…
0
Не, если что-то свое ставить в разрыв, то это можно что-нить типа бесконтактного датчика тока в корпусе на DIN-рейку. Отдельный счетчик слишком большой и слишком дорогой… А основное удовольствие — не ходить на лестничную клетку, чтобы снять показания — теряется, потому что кто его знает, синхронно они будут считать или нет.
Учитывая, что момент переключения между тарифами точно не синхронизируешь…
0
если не ошибаюсь… все интерфейсы и клеммы — находятся под одним щитком, который пломбируется
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.