Расскажу, как писал консольную программу для снятия показаний со счетчиков Меркурий 230 в торговом центре. Ссылка на исходники в конце статьи.
Началось собственно все со SCADA системы. Повезло нам получить проект на разработку и внедрение SCADA для торгового центра. Ну как повезло… В общем разработка этой системы отдельная история, которую, если читателю будет интересно, я расскажу и поделюсь набитыми шишками. Да и система еще полностью не принята, потому пока не будем светить подробностями.
Кратко:
— контроль температур в помещениях;
— централизованное управление вентиляцией по расписанию;
— управление подпиткой ГВС;
— учет воды и электричества;
— sms уведомления.
В этой статье об электричестве.
С выбором модели электрики слушали, потому тут особых проблем не испытал. Решил остановиться на Меркурии. Для 3х фаз – 234тые (электрики купили 230) для однофазной сети 206тая модель. Далее получилось, что электрики насовали по всему ТЦ только трехфазные счетчики. Ну мне только меньше проблем. Хотя не пойму зачем.
Программировал я до этого в основном ПЛК и небольшие скрипты на C#. Но тут решил, что смогу сам сделать энергоучет (хотелось опыта в программировании набраться). Погорячился, конечно.
Идея была такова:
— опросчик-сервер ведет энергоучет в базу данных;
— SCADA система отвечает за визуализацию
Способ опроса
Опрос реализовал просто – в бесконечном цикле по одному порту RS485. Вообще для работы системы диспетчеризации подобрал MOXA UPORT 1650-16. Под опрос отдал только один порт, но чтобы не создавать звезду (для RS-485 это не желательно) воспользовался повторителем. Странно, что буржуйских повторителей RS-485 с большим количеством входов не нашлось. Однако нашлась отечественная штуковина Тахион ПРТ-1/8(12) на 12 портов. Если получиться – выложу видео работы всей связки. Работает хорошо. Только к MOXA UPORT 1650-16 есть претензии, но это уже другая история.
Разработка самого опросчика
Про паттерны проектирования даже не слышал. Потому наделал ошибок сразу, т.к. увлекся наследованием (и не особо грамотно). По умным книжкам – надо было применять композицию. В дальнейшем надо будет переработать всю библиотеку.
Цепочка наследования получилась такая:
MeterDevice -> Mercury230 -> Mercury230_DatabaseSignals
MeterDevice – общий класс для всех счетчиков. Реализует обмен по COM порту c Modbus подобным протоколом;
Mercury230 – класс с набором функций опроса для конкретного счетчика;
Mercury230_DatabaseSignals – класс с конкретными параметрами счетчика (токи, напряжения и т.д.) и функцией их обновления. Наследование в нем было не удобным решением. Т.к. потом хлебнул проблем с сериализацей и десериализацией объектов. Но этот класс так прочно влез в код, что отступать было нельзя.
Ключевым в функциях опроса решил сделать структуру RXmes. В ней хранить результат ответа и сам массив байтов ответа. Любая функция опроса (например, запрос серийного номера) оперирует внутри себя этой структурой:
public enum error_type : int { none = 0,
AnswError = -5, // вернул один или несколько ошибочных ответов
CRCErr = -4,
NoAnsw = -2, // ничего не ответил на запрос после коннекта связи
WrongId = -3, // серийный номер не соответствует
NoConnect = -1 // отсутствие ответа
};
public struct RXmes
{
public error_type err;
public byte[] buff;
public byte[] trueCRC;
public void testCRC()
{
err = error_type.CRCErr;
if (buff.Length < 4)
{
err = error_type.CRCErr;
return;
}
byte[] newarr = buff;
Array.Resize(ref newarr, newarr.Length - 2);
byte[] trueCRC = Modbus.Utility.ModbusUtility.CalculateCrc(newarr);
if ((trueCRC[1] == buff.Last()) && (trueCRC[0] == buff[(buff.Length - 2)]))
{
err = error_type.none;
}
}
public void ReadArr(byte[] b)
{
buff = b;
testCRC();
}
}
public RXmes SendCmd(byte[] data)
{
RXmes RXmes_ = new RXmes();
byte[] crc = Modbus.Utility.ModbusUtility.CalculateCrc(data);
Array.Resize(ref data, data.Length + 2);
data[data.Length - 2] = crc[0];
data[data.Length - 1] = crc[1];
rs_port.Write(data, 0, data.Length);
System.Threading.Thread.Sleep(timeout);
if (rs_port.BytesToRead > 0)
{
byte[] answer = new byte[(int)rs_port.BytesToRead];
rs_port.Read(answer, 0, rs_port.BytesToRead);
RXmes_.ReadArr(answer);
if (RXmes_.err == error_type.none)
{
DataTime_last_contact = DateTime.Now;
}
return RXmes_;
}
RXmes_.err = error_type.NoConnect;
return RXmes_;
}
Таким образом, функция получения серийного номера счетчика Меркурий 230 получилась такая:
public byte[] GiveSerialNumber()
{
byte[] mes = {address, 0x08 , 0};
RXmes RXmes = SendCmd(mes);
if (RXmes.err == error_type.none) {
byte[] bytebuf = new byte[7];
Array.Copy(RXmes.buff, 1, bytebuf, 0, 7);
return bytebuf;
}
return null;
}
Кому интересно посмотреть другие функции – можно посмотреть исходники.
Протокол связи со SCADA
Изначально протокол простого TCP севера был простенький. Ответ SCADе по TCP выглядел для Меркурия 230того так.
«type=mеrc230*add=23*volt=1:221-2:221-3:221*cur=1:1.2-2:1.2-3:1.2»
Скадой данные парсились и выводились на иконку соответствующего счетчика
Все было бы хорошо, но заказчик решил (и уперся рогом), что ему все данные нужны в табличном виде. Да еще и захотел задавать лимиты всех параметров во время работы. А выход за лимиты должен индицироваться.
Порвав на себе волосы, т.к. SCADA табличные данные отображать не умела, сел писать отдельную программку для визуализации.
Свой протокол уже становился особо неудобным, т.к. количество параметров вырастало. Например, для тока появились верхний придел, состояние аварии, гистерезис включения аварии.
Получилось, что для параметров сформировался отдельный класс:
public MetersParameter() {
minalarm = false;
maxalarm = false;
}
public MetersParameter(float min, float max, float hist, float scalefactor = 1)
{
MinValue = min;
MaxValue = max;
Hist = hist;
minalarm = false;
maxalarm = false;
ScalingFactor = scalefactor;
}
public string alias{set; get;}
public float MaxValue { set; get; }
public float MinValue { set; get; }
public float ScalingFactor { set; get; } // коэффициент масштабирования. К примеру Коэффициент трансформации по току
public float Hist { set; get; }
private bool minalarm;
private bool maxalarm;
public bool ComAlarm { get { return MinValueAlarm || MaxValueAlarm ; } }
public virtual bool MinValueAlarm { get{
return minalarm;
} }
public virtual bool MaxValueAlarm { get{
return maxalarm;
} }
public virtual void RefreshData()
{
if (null != ParametrUpdated)
{
ParametrUpdated();
}
if ((MinValue == 0) && (MaxValue == 0))
{
return;
}
float calc_par = parametr * ScalingFactor;
if (calc_par < (MinValue - Hist))
{
minalarm = true;
}
if (calc_par > (MinValue + Hist))
{
minalarm = false;
}
if (calc_par < (MaxValue - Hist))
{
maxalarm = false;
}
if (calc_par > (MaxValue + Hist))
{
maxalarm = true;
}
}
float parametr;
public bool UseScaleForInput = false;
public virtual float Value {
set{
parametr = UseScaleForInput ? value / (ScalingFactor <= 0 ? 1 : ScalingFactor) : value;
RefreshData();
}
get
{
return parametr * ScalingFactor;
}
}
public void CopyLimits(MetersParameter ext_par)
{
this.MinValue = ext_par.MinValue;
this.MaxValue = ext_par.MaxValue;
this.Hist = ext_par.Hist;
}
}
Тут выручила сериализация объектов. Попробовав Байтовую, XML и JSON сериализацию, было решено остановиться на JSON (DataContractJsonSerializer). Она удобно читалась глазом, объем данных получался меньше XML. И вообще DataContractJsonSerializer прощал отсутствие конструктора без аргументов. Это значительно упрощало жизнь.
База данных
Конечно, важнейшим моментом было — запись показаний счётчиков. Т.к. Scada система работала с MySql, то и опросчик было решено завязывать с ней. Тут особых проблем не было.
Вопрос был только один – «какие данные записывать?», т.к. вариантов счетчик дает не мало. Собственно коды для запроса:
public enum peroidQuery : byte
{
afterReset = 0x0,
thisYear = 1,
lastYear = 2,
thisMonth = 3, thisDay = 4, lastDay = 5,
thisYear_beginning = 9,
lastYear_beginning = 0x0A,
thisMonth_beginning = 0x0B,
thisDay_beginning = 0x0C,
lastDay_beginning = 0x0D
}
Изначально было решено записывать потребление за месяц и за сутки. Вдобавок, было реализован простенький механизм снятия показаний по месяцам за год. И контроль наличия этих данных. Если данных не хватало – они дописывались.
Но уже ближе к концу я получаю требование, чтобы в отчетах всегда было написаны показания на начало и на конец периода. Людям, нанятым на обслуживание, не нравилась одна колонка «Потреблено». Пришлось добавлять считывание на начало месяца и суток.
Итог
Получилась немного костыльная, но все же рабочая программа учета электроэнергии. На данный момент программка опрашивает около 70 счетчиков. Консольное приложение крутится на сервере, а клиентская часть работает на АРМе пользователя.
Исходник опросчика выкладываю на GitHub. Ссылку на клиентскую часть постараюсь выложить позже.
P.S. Про сходство протокола Меркурия 230 и СЭТ-4тм
Если кто не сталкивался. То есть такой Завод им. Фрунзе (в Нижнем Новгороде). И счетчики у них работают с очень похожим протоколом. Пробегался по мануалам обоих – один в один. Но, слышал, что в протоколах есть какие-то различия (пока не вдавался). Жаль, что на руках нет СЭТа.
Ноги сходства растут из того, что Меркурии разработаны бывшими работниками Фрунзе. Такие дела. Странно, почему на слуху больше Меркурий.
Автор: levWi