Расскажу, как писал консольную программу для снятия показаний со счетчиков Меркурий 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тм


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

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


  1. Igorjan
    15.06.2017 22:09
    +1

    Судя по отсутствию жалоб на бумажную волокиту, учет не коммерческий?
    Проверяется ли разница в показаниях на вводе и сумм у конечных потребителей?

    по «звездатору» — использовали такой


    1. levWi
      15.06.2017 22:40
      +1

      Вот за такой девайс — спасибо. Не находил.

      Если под коммерческой понимается выставление счетов арендаторам, то коммерческий. Уже генерацию делать можно по потребителям и с привязкой к «административным зонам» (как я их называю). Генерирую им в формате *.html

      Требование к проверкам разницы ввода и конечных потребителей было. Только мне до сих пор не могут дать «архитектуру» сети. ТЦ как бы уже работает. Только что откуда запитано не совсем понятно (подрядчика, который монтировал прогнали, да и исполнительную документацию нормальную не получили). Пока думаю, как расчет сделать гибко.

      Похожая проблема была с водоучетом. Но с ним уже сделал им и контроль суммы. Правда выданная мне формула от «главного инженера» объекта настолько глупая, что всегда будет давать 0 кубов разницы.


      1. andranick
        16.06.2017 18:26

        Вот как бы не советую использовать словосочетание «коммерческий учет». Технический. А то к системам АСКУЭ и требования нехилые, и спрос не детский. А АСТУЭ — для внутреннего потребления заказчика, если его все устраивает, — наздоровье, это дело только заказчика. Типа чтоб для съема показаний по счетчикам «бегунка» не посылать.


        1. levWi
          16.06.2017 18:40

          Значит АСТУЭ. Подходит


  1. NikitaYakuntsev
    15.06.2017 22:09

    Спасибо.
    Не увидел в статье — какую SCADA-систему Вы использовали?


    1. levWi
      15.06.2017 22:18
      +2

      Scada+. На хабре есть. О ней постараюсь попозже написать


  1. Romer
    16.06.2017 09:55

    Поздравляю с дебютом! Приятно видеть появление статей тех, кто работает на твоем решении.
    Насчет статьи про Scada+ — будет очень полезно, со своей стороны постараюсь не остаться в долгу и обещаю бонусы!
    По некоторым моментам, которые прочитал в статье применительно к скаде (например про табличный вывод) — напишу уже Вам напрямую, есть отработанные решения как это сделать средствами скады.


    1. levWi
      16.06.2017 16:06

      В том то и проблема — вы как разработчик лучше понимаете, как рантайм SCADA+ работает. Знаете подводные камни. А я как с черным ящиком работаю, боюсь чего-то добавлять — чтобы система валиться не начала.

      В любом случае от заказчика уже неприятный осадок остался. Не хочу им больше ничего делать. Пускай так сидят.

      Постараюсь расписать что мне понравилось, что нет (с точки зрения пользователя). Благо довелось запустить проект с MasterSCADA — есть с чем сравнить.


  1. ajratr
    16.06.2017 15:47

    Скажите пожалуйста, а сколько времени ушло на разработку?


    1. levWi
      16.06.2017 15:51

      Сложно сказать. ТЦ с 14го года строится… Параллельно мы разные объекты делали.
      Наверно чистого времени 2-3 месяца по АСКУЭ… По SCADе дольше, и не всегда по нашей вине


      1. ajratr
        16.06.2017 16:45

        А почему не воспользовались готовым решением?
        Та же Simple Scada с OPC сервером предлагается по очень привлекательной цене.


        1. levWi
          16.06.2017 18:34

          Не видел их цеников. 9 параметров (ток напряжение косинус) на счетчик * 70 счетчиков это уже 630 точек.
          Тогда уже надо брать готовую программу учета и крутить ее отдельно — мне кажется дешевле будет.


          1. andranick
            16.06.2017 18:56

            Ну как бы ток, напряжение и косинус относятся к оперативным параметрам (измерениям текущим), их малоинтересно опрашивать, если в цепочке последовательно 70 счетчиков, слишком большой период опроса. Тогда уж, возможно, интереснее вытягивать данные, которые интегрируются самим счетчиком за определенный период — профили мощности за 30 минут, потребленная активная/реактивная энергия накопительным итогом (со сброса), на начало месяца, с начала месяца и т.п. — эти параметры можно вычитывать достаточно долго и для целей технического учета электроэнергии этого достаточно. Есть проект, в котором наверх тянутся и оперативные, и интегральные параметры. Так заказчиком оперативные используются только как «для справки», в основном используются профили мощности для контроля потребления и параметры энергии — для генерации отчетов.
            Так что точек может быть и значительно больше 630.


            1. levWi
              16.06.2017 19:31

              Тут тоже только для справки. Забавно что с нас эти параметры требовали. Мы сделали. Но ими не пользуются сейчас. Пока что только отчёты генерируют


  1. Cuthbertnogood
    16.06.2017 17:57

    Все таки это не АКУЭ.


    1. levWi
      16.06.2017 18:37

      Это и не утверждается


  1. Siemargl
    16.06.2017 23:02

    SCADA и АИИСКУЭ ортогональны. Совмещать одно с другим неудобно.

    Потому СКАДы не предназначены для задач учета, и наоборот.