Страница 1 из 7 123 ... ПоследняяПоследняя
Показано с 1 по 10 из 62

Тема: Modbus-мастер в Visual Studio (.NET)

  1. #1
    Пользователь
    Регистрация
    13.10.2011
    Адрес
    Златоуст
    Сообщений
    1,021

    По умолчанию Modbus-мастер в Visual Studio (.NET)

    Ща будем подрубать Овеновский ПЛК110 к своей проге в Visual Studio. Полтора года назад мы этим уже занимались, но молча. Будем исправляться.

    Должно получиться окошко, отображающее число циклов контроллера (счёт на стороне ПЛК). Также в окошке будет кнопка, позволяющая сбросить счётчик. Начнём с консольной программы, которая будет только выводить число. Закончим WPF-приложением с переходом от опросной модели к событийной.

    Затариваемся

    1. Visual Studio. Например, версия 2013 Community Edition — самая навороченная из бесплатных для некоммерческого использования. Также можно не выпендриваться и скачать самую лёгкую редакцию Express — ею можно пользоваться и в коммерческих целях. До первого запуска как-нибудь сами дойдёте, ок? Я верю в вас.
    2. Библиотека NModbus. Пять лет уже не обновлялась, но работает нормально, да и опенсорс, есличо. Бесплатна для любых целей. Качаем ".NET 3.5 NModbus 1.11.0.0 Binaries". Можно не качать, если Visual Studio у вас Community Edition или ещё серьёзнее.


    Готовимся
    Пока устанавливается студия, можно купить контроллер Овен ПЛК110-60 и написать для него тестовую программу. Начнём с Modbus TCP, но потом я расскажу, как сделать RTU, например.

    В общем, тут ничего хитрого: в конфигурацию добавляем Modbus (slave), в этот Modbus (slave) добавляем регистр 2 byte и называем его reg0, а в Modbus [FIX] добавляем TCP. Скриншот:

    01.png

    А в PLC_PRG пишем reg0 := reg0 + 1; Всё, тестовая программа готова. См. файл test0.pro, но он именно для ПЛК110-60. Если у вас другой — делайте сами. Ну и понятно, что можно взять любое другой Modbus-устройство.

    Фигачим
    В наконец запустившейся студии делаем консольное приложение на C#.

    02.png

    Теперь надо добавить в проект библиотеку NModbus. Тут есть два способа:
    1. Если у вас студия Community Edition или круче, то воспользуемся NuGet. Tools > NuGet Package Manager > Manage NuGet Packages... В появившемся окне ищем NModbus:

      03.png

      Жмём Install. Всё, в проекте появилась ссылка "NModbus4":

      04.png
    2. Ручками. Из ранее скачанного архива NModbus достаём папку bin/net и из неё добавляем Modbus, log4net и Unme.Common в ссылки проекта:

      05.png


    Теперь можно написать мастера сети (айпишник своего ПЛК в длинную строчку подставьте):
    Код:
    using Modbus.Device;
    using System;
    using System.Net.Sockets;
    using System.Threading.Tasks;
    
    namespace NModbusTut
    {
        class Program
        {
            static void Main(string[] args)
            {
                var master = ModbusIpMaster.CreateIp(new TcpClient("10.1.6.10", 502));
                while (true)
                {
                    Console.Write("\r{0}", master.ReadHoldingRegisters(1, 0, 1)[0]);
                }
            }
        }
    }
    Хыы, работает:

    06.png

    См. NModbusTut01.zip.

    Само собой, ModbusIpMaster можно заменить на ModbusSerialMaster, скормить ему вместо TcpClient последовательный порт (SerialPort), и таким образом получить Modbus RTU вместо Modbus TCP.

    Графика
    Теперь сотворим что-нибудь гуёвое.

    07.png

    На событие окна Loaded ставим вот такой обработчик:
    Код:
    mbMaster = ModbusIpMaster.CreateIp(new TcpClient("10.1.6.10", 502));
    masterLoop = Task.Factory.StartNew(new Action(Update));
    Ну и следом метод с полями:
    Код:
    ModbusIpMaster mbMaster;
            Task masterLoop;
    
            private void Update()
            {
                while (true)
                {
                    Dispatcher.Invoke(new Action(delegate()
                    {
                        progressBar.Value = mbMaster.ReadHoldingRegisters(1, 0, 1)[0];
                    }));
                }
            }
    И оно даже работает (файл NModbusTutWPF01.zip):

    08.png
    Но так делать — не совсем ок. То есть совсем не ок. Видите, как в методе Update мы десятки раз в секунду идём в диспетчерский поток вне зависимости от того, изменился регистр или нет? Давайте-ка перепишем так, чтобы воспользоваться привязкой к данным и при этом не гонять её за зря. Дальше чуть сложнее.

    Что нужно исправить и добавить? Контролы в WPF любят сами привязываться к источникам данных. А у нас вместо этого класс окна кормит зелёную полоску с ложечки и называет её по имени. Надо, чтобы был такой объект, который принимал бы на вход модбас-регистры много раз в секунду, а отдавал теги заинтересованным контролам только когда теги изменяются. При этом знать контролы поимённо этот объект не должен, иначе добавление контролов будет затруднено. Делаем вот такой класс:
    Скрытый текст:
    Код:
    public enum Tag
    {
        Reg0
    }
    
    /// <summary>
    /// Готов принимать сырые регистры по опросной модели и преобразовывать
    /// их в теги с уведомлением об изменениях по событийной модели
    /// </summary>
    /// <remarks>
    /// Для простоты привязки сделан динамическим объектом. Свойства = теги.
    /// </remarks>
    public class Depoller : DynamicObject, INotifyPropertyChanged
    {
        // Сюда новые и старые значения тегов
        IDictionary<tag, object=""> oldValues, newValues;
    
        // Сюда названия тегов, изменившихся на данном проходе
        List<tag> dirtyTags = new List<tag>();
    
        // Через это событие другие объекты узнают об изменениях
        public event PropertyChangedEventHandler PropertyChanged;
    
        Dispatcher dispatcher;
    
        public Depoller(Dispatcher d)
        {
            dispatcher = d;
            oldValues = new Dictionary<tag, object="">();
            newValues = new Dictionary<tag, object="">();
        }
    
        /// <summary>
        /// Разбирает Modbus-регистры на теги
        /// </summary>
        /// 
    Modbus-регистры как есть
        public void Input(ushort[] data)
        {
            // Парсим
            newValues[Tag.Reg0] = data[0];
    
            // Составляем список изменившихся
            lock (dirtyTags)
            {
                // Новый список
                dirtyTags.Clear();
    
                foreach (var pair in newValues)
                {
                    Object val = null;
                    // Если тег записывается впервые или если он изменился
                    if (!oldValues.TryGetValue(pair.Key, out val) || !pair.Value.Equals(val))
                    {
                        oldValues[pair.Key] = pair.Value; // Запоминаем на следующий раз
                        dirtyTags.Add(pair.Key); // Добавляем в список изменённый на этот раз
                    }
                }
            }
            if (PropertyChanged != null) // Если есть с кем поделиться этой радостной вестью
            {
                // В потоке пользовательского интерфейса
                dispatcher.BeginInvoke(new Action(delegate()
                {
                    lock (dirtyTags)
                    {
                        // Сообщить о каждом изменённом теге
                        foreach (var tag in dirtyTags)
                        {
                            PropertyChanged(this, new PropertyChangedEventArgs(tag.ToString()));
                        }
                    }
                }));
            }
        }
    
        /// <summary>
        /// Доступ к тегам как к свойствам объекта
        /// </summary>
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            return oldValues.TryGetValue((Tag)Enum.Parse(typeof(Tag), binder.Name), out result);
        }
    }
    В перечислении Tag именуем свои теги. В методе Input разбираем регистры на теги. Скажем, если один тег типа float у вас передаётся двумя модбас-регистрами (типично), то именно в этом методе из двух ushort'ов (WORD'ов) получается один float (REAL). Дальше немножко магии, и у нас есть объект, который сообщает об изменениях тегов и отдаёт эти теги по запросу. Теперь в классе окна (а в серьёзном проекте лучше где-нибудь повыше) мы скармаливаем регистры уже объекту этого класса и задаём привязку контролов на него. Можно не ковыряя лишний раз код добавить текстовое поле только за счёт разметки, например:

    09.png
    См. NModbusTutWPF02.zip.

    Про кнопку, вызывающую действие на ПЛК — во второй части. Stay tuned.</tag,></tag,></tag></tag></tag,>
    Вложения Вложения
    Последний раз редактировалось Yegor; 20.12.2014 в 09:55.

  2. #2

    По умолчанию

    Большой respect тебе, Yegor. Отличная тема.

  3. #3

    По умолчанию

    а с платф .NET 4.0 не будет работать?nModbus.JPG
    Последний раз редактировалось rekbrjaaa; 24.12.2015 в 14:00.

  4. #4

    По умолчанию

    Yegor, спасибо за пример.

    Не могу найти продолжение "Про кнопку, вызывающую действие на ПЛК" - можете дать ссылку?

    Заранее спасибо!

  5. #5
    Пользователь
    Регистрация
    13.10.2011
    Адрес
    Златоуст
    Сообщений
    1,021

    По умолчанию

    Да я так и не собрался написать. Там всё сводится к вызову функций записи по нажатию кнопки типа master.Write... Хотел ещё рассказать про развязку кнопок (gui) и обработчиков а ля MVVM, но это до меня всё расписано вдоль и поперёк.

  6. #6

    По умолчанию

    Да, спасибо!

    master.WriteSingleRegister(1, 0, 96)
    1 - адрес слейва
    0 - адрес регистра
    96 - значение


    Продолжаю усложнять себе задачи
    Пытаюсь подать/считать сигнал с входа/выхода.
    Как я понимаю, для этого нужно использовать Writesinglecoil Readcoilstatus
    Но не могу найти адреса для входов/выходов - в инструкции не нашел (у меня ПЛК110-24.60.Р.М).
    Не подскажете как обращатся к входу/выходу ПЛК. Я так понимаю должны быть какая-то таблица "Вход/выход ПЛК" соответствует "Номер бита"?

  7. #7
    Пользователь
    Регистрация
    13.10.2011
    Адрес
    Златоуст
    Сообщений
    1,021

    По умолчанию

    Такой таблицы нет. Вообще говоря слейв, который вы создаёте в ПЛК для связи с ПК, не имеет связей со входами и выходами. Эти связи вам нужно создать самому. По-простому можно в программе ПЛК самому разметить выходы через присваивание типа di10 := slaveRegA.9.

    В боевых проектах мне приходится делать сложнее. Нулевой регистр отвожу на код команды, ещё несколько — на аргументы. Дальше в программе ПЛК пишу интерпретатор вроде
    Код:
    TYPE CMD: (NOP, ON, OFF); END_TYPE
    
    
    CASE pc_cmd OF
    	ON: DI1 := TRUE;
    	OFF: DI1 := FALSE;
    END_CASE
    pc_cmd := NOP;
    При этом в мастере есть такой же
    Код:
    enum Cmd { Nop, On, Off }
    И, соответственно, я могу командовать контроллером примерно как master.WriteSingleRegister(1, 0, Cmd.On). Ну, естественно, там добавляются ещё аргументы и т.д.

  8. #8

    По умолчанию

    Yegor, а есть где-то описание "ПЛК110/Мx110 memory model"?

    В частности:
    1) word tearing
    2) out of order writes / happens-before / data race
    3) out of thin air values

    Насколько я понимаю, в момент работы цикла "сетевые переменные" своих состояний не меняют. Но как оно работает в момент "до цикла/после цикла"?
    Какие-нибудь гарантии на tearing есть?

  9. #9
    Пользователь
    Регистрация
    13.10.2011
    Адрес
    Златоуст
    Сообщений
    1,021

    По умолчанию

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

  10. #10

    По умолчанию

    Цитата Сообщение от Yegor Посмотреть сообщение
    Такой таблицы нет. Вообще говоря слейв, который вы создаёте в ПЛК для связи с ПК, не имеет связей со входами и выходами. Эти связи вам нужно создать самому.
    Спасибо, этого понимания мне не хватало.
    Пока буду пользоваться более простым способом (напрямую задавать di10 := slaveRegA.9)

    Столкнулся с еще одной проблемой, после запуска своей программы и выполнения какого-то действия (запись или чтение) я не могу подключится к ПЛК из Codesys.

    ПЛК у меня подключен по Ethernet (Ip, шлюз и маска прописаны согласно настроек локальной сети).
    При подключении из Codesys получаю ошибку: "Ошибка связи (#0): произошло отключение".

    Также если потом пытаюсь подключится к ПЛК из своей программы например для чтения (или Вашего консольного приложения) как правило перед успешным подключением получаю две ошибки: "Конечный компьютер отверг запрос на подключение".

    Возможно нужно закрывать подключение? Но в Вашем проекте Вы ничего подобного не делаете и ошибок нет.

Страница 1 из 7 123 ... ПоследняяПоследняя

Похожие темы

  1. Помогите с ActiveX в Microsoft Visual Studio 2005
    от Alex_31 в разделе Master SCADA 3
    Ответов: 4
    Последнее сообщение: 06.10.2013, 09:55
  2. ModBUS TCP ПЛК100(мастер) и Lectus(слейв)
    от GSK в разделе Сетевые технологии
    Ответов: 4
    Последнее сообщение: 14.09.2012, 11:35
  3. Modbus Universal MasterOPC Server и ПЛК мастер
    от smk1635 в разделе Master SCADA 3
    Ответов: 7
    Последнее сообщение: 13.09.2011, 22:22
  4. СМИ1, modbus-rtu, мастер. Команда 4. КАК?
    от Matysik в разделе Панели оператора (HMI)
    Ответов: 2
    Последнее сообщение: 26.05.2011, 15:32

Ваши права

  • Вы не можете создавать новые темы
  • Вы не можете отвечать в темах
  • Вы не можете прикреплять вложения
  • Вы не можете редактировать свои сообщения
  •