Расскажу, как писал консольную программу для снятия показаний со счетчиков Меркурий 230 в торговом центре. Ссылка на исходники в конце статьи.

Началось собственно все со SCADA системы. Повезло нам получить проект на разработку и внедрение SCADA для торгового центра. Ну как повезло… В общем разработка этой системы отдельная история, которую, если читателю будет интересно, я расскажу и поделюсь набитыми шишками.

Кратко:

— контроль температур в помещениях;
— централизованное управление вентиляцией по расписанию;
— управление подпиткой ГВС;
— учет воды и электричества;

В этой статье об электричестве.

С выбором модели электрики слушали, потому тут особых проблем не испытал. Решил остановиться на Меркурии. Для 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тм


Если кто не сталкивался. То есть такой Завод им. Фрунзе (в Нижнем Новгороде). И счетчики у них работают с очень похожим протоколом. Пробегался по мануалам обоих – один в один. Но, слышал, что в протоколах есть какие-то различия (пока не вдавался). Жаль, что на руках нет СЭТа.
Ноги сходства растут из того, что Меркурии разработаны бывшими работниками Фрунзе. Такие дела. Странно, почему на слуху больше Меркурий.