Ща будем подрубать Овеновский ПЛК110 к своей проге в Visual Studio. Полтора года назад мы этим уже занимались, но молча. Будем исправляться.
Должно получиться окошко, отображающее число циклов контроллера (счёт на стороне ПЛК). Также в окошке будет кнопка, позволяющая сбросить счётчик. Начнём с консольной программы, которая будет только выводить число. Закончим WPF-приложением с переходом от опросной модели к событийной.
Затариваемся
Visual Studio. Например, версия 2013 Community Edition — самая навороченная из бесплатных для некоммерческого использования. Также можно не выпендриваться и скачать самую лёгкую редакцию Express — ею можно пользоваться и в коммерческих целях. До первого запуска как-нибудь сами дойдёте, ок? Я верю в вас.
Библиотека 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. Скриншот:
А в PLC_PRG пишем reg0 := reg0 + 1; Всё, тестовая программа готова. См. файл test0.pro, но он именно для ПЛК110-60. Если у вас другой — делайте сами. Ну и понятно, что можно взять любое другой Modbus-устройство.
Фигачим
В наконец запустившейся студии делаем консольное приложение на C#.
Теперь надо добавить в проект библиотеку NModbus. Тут есть два способа:
Если у вас студия Community Edition или круче, то воспользуемся NuGet. Tools > NuGet Package Manager > Manage NuGet Packages... В появившемся окне ищем NModbus:
Само собой, ModbusIpMaster можно заменить на ModbusSerialMaster, скормить ему вместо TcpClient последовательный порт (SerialPort), и таким образом получить Modbus RTU вместо Modbus TCP.
Вложение 15802
Но так делать — не совсем ок. То есть совсем не ок. Видите, как в методе 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). Дальше немножко магии, и у нас есть объект, который сообщает об изменениях тегов и отдаёт эти теги по запросу. Теперь в классе окна (а в серьёзном проекте лучше где-нибудь повыше) мы скармаливаем регистры уже объекту этого класса и задаём привязку контролов на него. Можно не ковыряя лишний раз код добавить текстовое поле только за счёт разметки, например:
Не могу найти продолжение "Про кнопку, вызывающую действие на ПЛК" - можете дать ссылку?
Заранее спасибо!
12.03.2016, 06:05
Yegor
Да я так и не собрался написать. Там всё сводится к вызову функций записи по нажатию кнопки типа master.Write... Хотел ещё рассказать про развязку кнопок (gui) и обработчиков а ля MVVM, но это до меня всё расписано вдоль и поперёк.
12.03.2016, 18:06
greenwod
Да, спасибо!
master.WriteSingleRegister(1, 0, 96)
1 - адрес слейва
0 - адрес регистра
96 - значение
Продолжаю усложнять себе задачи :)
Пытаюсь подать/считать сигнал с входа/выхода.
Как я понимаю, для этого нужно использовать Writesinglecoil Readcoilstatus
Но не могу найти адреса для входов/выходов - в инструкции не нашел (у меня ПЛК110-24.60.Р.М).
Не подскажете как обращатся к входу/выходу ПЛК. Я так понимаю должны быть какая-то таблица "Вход/выход ПЛК" соответствует "Номер бита"?
12.03.2016, 18:26
Yegor
Такой таблицы нет. Вообще говоря слейв, который вы создаёте в ПЛК для связи с ПК, не имеет связей со входами и выходами. Эти связи вам нужно создать самому. По-простому можно в программе ПЛК самому разметить выходы через присваивание типа 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). Ну, естественно, там добавляются ещё аргументы и т.д.
12.03.2016, 23:31
Владимир Ситников
Yegor, а есть где-то описание "ПЛК110/Мx110 memory model"?
В частности:
1) word tearing
2) out of order writes / happens-before / data race
3) out of thin air values
Насколько я понимаю, в момент работы цикла "сетевые переменные" своих состояний не меняют. Но как оно работает в момент "до цикла/после цикла"?
Какие-нибудь гарантии на tearing есть?
13.03.2016, 09:08
Yegor
Не попадалось. Да и не наблюдал таких эффектов. Но я не загружал ПЛК запросами настолько, чтобы это могло проявиться. Ну а модули вообще быстрого дуплексного интерфейса не имеют, чтобы это можно было хотя бы проверить (не говоря о том, чтобы столкнуться с этим в реальном проекте). Если в контексте темы, то лучше, конечно, приостанавливать опрос на время записи — так можно хотя бы себя обезопасить от вероятного рассинхрона.
14.03.2016, 12:21
greenwod
Цитата:
Сообщение от Yegor
Такой таблицы нет. Вообще говоря слейв, который вы создаёте в ПЛК для связи с ПК, не имеет связей со входами и выходами. Эти связи вам нужно создать самому.
Спасибо, этого понимания мне не хватало.
Пока буду пользоваться более простым способом (напрямую задавать di10 := slaveRegA.9)
Столкнулся с еще одной проблемой, после запуска своей программы и выполнения какого-то действия (запись или чтение) я не могу подключится к ПЛК из Codesys.
ПЛК у меня подключен по Ethernet (Ip, шлюз и маска прописаны согласно настроек локальной сети).
При подключении из Codesys получаю ошибку: "Ошибка связи (#0): произошло отключение".
Также если потом пытаюсь подключится к ПЛК из своей программы например для чтения (или Вашего консольного приложения) как правило перед успешным подключением получаю две ошибки: "Конечный компьютер отверг запрос на подключение".
Возможно нужно закрывать подключение? Но в Вашем проекте Вы ничего подобного не делаете и ошибок нет.
14.03.2016, 12:58
Yegor
Выкладывайте проекты — будем смотреть в чём дело.
По-хорошему закрывать подключение, конечно, надо. И обрабатывать отключение извне тоже.
14.03.2016, 16:45
greenwod
Я после внесения правок в код скорее всего загружал только изменения, а нужно "Загружать все" - ошибка из-за этого была (только таким образом у меня получилось повторить ошибку).
27.04.2016, 20:36
voale
Цитата:
Сообщение от greenwod
Да, спасибо!
master.WriteSingleRegister(1, 0, 96)
1 - адрес слейва
0 - адрес регистра
96 - значение
Продолжаю усложнять себе задачи :)
Пытаюсь подать/считать сигнал с входа/выхода.
Как я понимаю, для этого нужно использовать Writesinglecoil Readcoilstatus
Но не могу найти адреса для входов/выходов - в инструкции не нашел (у меня ПЛК110-24.60.Р.М).
Не подскажете как обращатся к входу/выходу ПЛК. Я так понимаю должны быть какая-то таблица "Вход/выход ПЛК" соответствует "Номер бита"?
greenwod
можешь выложить проект , интересно как ты справился с задачей
"Пытаюсь подать/считать сигнал с входа/выхода."
спасибо
и еще вопрос: не возникают ли у тебя проблемы как тут http://www.cyberforum.ru/csharp-net/thread388326.html
, т.е опрос идет исправно, а вот ответы на запросы, происходят только первые 35
28.04.2016, 09:05
voale
Цитата:
Сообщение от greenwod
Да, спасибо!
master.WriteSingleRegister(1, 0, 96)
1 - адрес слейва
0 - адрес регистра
96 - значение
Продолжаю усложнять себе задачи :)
Пытаюсь подать/считать сигнал с входа/выхода.
Как я понимаю, для этого нужно использовать Writesinglecoil Readcoilstatus
Но не могу найти адреса для входов/выходов - в инструкции не нашел (у меня ПЛК110-24.60.Р.М).
Не подскажете как обращатся к входу/выходу ПЛК. Я так понимаю должны быть какая-то таблица "Вход/выход ПЛК" соответствует "Номер бита"?
greenwod
можешь выложить проект , интересно как ты справился с задачей
"Пытаюсь подать/считать сигнал с входа/выхода."
спасибо
и еще вопрос: не возникают ли у тебя проблемы как тут http://www.cyberforum.ru/csharp-net/thread388326.html
, т.е опрос идет исправно, а вот ответы на запросы, происходят только первые 35
28.04.2016, 09:15
voale
Цитата:
Сообщение от greenwod
Да, спасибо!
master.WriteSingleRegister(1, 0, 96)
1 - адрес слейва
0 - адрес регистра
96 - значение
Продолжаю усложнять себе задачи :)
Пытаюсь подать/считать сигнал с входа/выхода.
Как я понимаю, для этого нужно использовать Writesinglecoil Readcoilstatus
Но не могу найти адреса для входов/выходов - в инструкции не нашел (у меня ПЛК110-24.60.Р.М).
Не подскажете как обращатся к входу/выходу ПЛК. Я так понимаю должны быть какая-то таблица "Вход/выход ПЛК" соответствует "Номер бита"?
greenwod
можешь выложить проект , интересно как ты справился с задачей
"Пытаюсь подать/считать сигнал с входа/выхода."
спасибо
и еще вопрос: не возникают ли у тебя проблемы как тут http://www.cyberforum.ru/csharp-net/thread388326.html
, т.е опрос идет исправно, а вот ответы на запросы, происходят только первые 35
Там неправильно организован приём ответа. Вообще прежде чем писать низкоуровневый обмен, надо запомнить:
что ответ может прийти не сначала;
что ответ может прийти не до конца;
что ответ может разбиться на несколько частей;
что несколько ответов могут прийти разом;
что может прийти вообще не ответ;
что ответ может не прийти;
что ответ может прийти через запрос;
что вместо ответа или вместе с ответом может прийти что угодно даже без запроса.
Если неохота всё это контролировать и вообще страшно покидать зону комфорта «запрос-ответ», то лучше пользоваться высокоуровневыми проверенными библиотеками а ля NModbus.
13.05.2016, 15:00
fillpackart
Yegor, вы не могли бы подсказать, как мне решить следующую проблему?
Есть сеть ПЛК63, подключенная к gprs модему Невод. Сам модем настроен на передачу данных на белый айпи у меня дома. Раньше я использовал программное обеспечение геолинка(прозводитель модема), которое эмулировало com-порт из соединения с модемом, и я мог его опрашивать вот таким образом:
var r = new ModbusSerialMaster(myPort);
r.ReadHoldingRegisters(10, 4098, 1);
Теперь же мне нужно осуществлять связь с сетью плк НЕ используя стороннее ПО. Отсюда вопрос - как мне из кода c# в Visual Studio (.NET) связаться со своими ПЛК?
17.05.2016, 10:05
voale
Цитата:
Сообщение от Yegor
Такой таблицы нет. Вообще говоря слейв, который вы создаёте в ПЛК для связи с ПК, не имеет связей со входами и выходами. Эти связи вам нужно создать самому. По-простому можно в программе ПЛК самому разметить выходы через присваивание типа di10 := slaveRegA.9.
В боевых проектах мне приходится делать сложнее. Нулевой регистр отвожу на код команды, ещё несколько — на аргументы. Дальше в программе ПЛК пишу интерпретатор вроде[CODE]TYPE CMD: (NOP, ON, OFF); END_TYPE
подскажите плиз
slaveRegA.9 откуда берется?
и нулевой регистр на код команды это как будет?
18.05.2016, 06:34
Yegor
Цитата:
Отсюда вопрос - как мне из кода c# в Visual Studio (.NET) связаться со своими ПЛК?
Попробуйте создавать мастера через ModbusSerialMaster.CreateRtu(TcpClient client) (либо CreateAscii по ситуации). Тогда обмен будет в формате последовательного порта, но канал останется TCP. Это вместо традиционного ModbusIpMaster.CreateIp, где формат сообщений иной.
Цитата:
подскажите плиз
slaveRegA.9 откуда берется?
и нулевой регистр на код команды это как будет?
Всё это вы задаёте сами в конфигурации ПЛК. То есть добавляете туда модбас-слейв, добавляете в него регистры, именуете их подходящим образом.
24.05.2016, 11:09
madiggo
Коллеги, при использовании NModbus4 из NuGet столкнулся с проблемой, когда при пропадании связи (банально выдернут шнур) программа надолго зависала. Как оказалось, необходимо установить свойство ModbusIPMaster.Transport.ReadTimeout в какое-то разумное значение (в мс). По умолчанию там -1. Поставил 500 - работает, при выдергивании шнура исправно выкидывает исключение. Там ещё есть свойство WriteTimeout, тоже его надо установить во что-то приемлемое.
Проблема не ОВЕН-ориентированная, а NModbus4, но вдруг кому пригодится.
25.05.2016, 21:26
Yegor
А я вот узнал, что NModbus не умеет делать несколько слейвов на одном TCP-канале. Печаль-беда.
26.05.2016, 11:52
madiggo
Цитата:
Сообщение от Yegor
А я вот узнал, что NModbus не умеет делать несколько слейвов на одном TCP-канале. Печаль-беда.
А ещё по ModbusIPMaster нельзя сказать, в каком состоянии соединение, нет никаких свойств, можно только попробовать сделать запрос и поймать исключение.
26.05.2016, 19:49
Yegor
Что именно вы подразумеваете? Как такие свойства по-вашему должны выглядеть и работать?
27.05.2016, 11:10
madiggo
Цитата:
Сообщение от Yegor
Что именно вы подразумеваете? Как такие свойства по-вашему должны выглядеть и работать?
Ну хотя бы на чтение TcpClient. Конечно, его можно сначала создать и передать в конструктор его переменную. Но логичнее было бы получать из ModbusIPMaster. Зачем нужно? Чтобы без исключительной ситуации отлавливать отвалившееся соединение хотя бы.
27.05.2016, 17:55
Yegor
Есть подозрение, что вы что-то не так делаете. Tell, don't ask. По принципу инкапсуляции, вас не должно беспокоить состояние мастера и уж тем более свойства его транспорта. Вы просто должны быть готовы к тому, что мастер выдаст исключение. Отлавливать исключения проще, чем поддерживать чужое состояние (stateful vs stateless).
30.05.2016, 11:51
greenwod
Цитата:
Сообщение от voale
greenwod
можешь выложить проект , интересно как ты справился с задачей
"Пытаюсь подать/считать сигнал с входа/выхода."
спасибо
и еще вопрос: не возникают ли у тебя проблемы как тут http://www.cyberforum.ru/csharp-net/thread388326.html
, т.е опрос идет исправно, а вот ответы на запросы, происходят только первые 35
Здравствуйте!
Я еще не успел написать что-то, что не стыдно выложить на форум :)
Вы присваиваете имя входу ПЛК (например in1).
Создаете в MODBUS (slave) регистр (например reg0).
В коде ПЛК присваиваете в нужный бит регистра значение своего входа:
reg0.2:=in1; (где .2 номер бита регистра).
Важно помнить, что сигнал может быть коротким и Ваша программа может не успеть его считать - я делал дополнительный регистр в котором контролировал сигнал.
В коде программы считываете значение регистра:
bool[] coilstatus = master.ReadCoils(1 , Bit , 1) [0]
Где Bit – адрес бита (в нашем случае будет 2).
Насчет проблем с первыми 35 запросами не стыкался. Используйте Nmodbus как описано вначале данной темы и проблем быть не должно.
30.05.2016, 14:00
greenwod
Цитата:
Сообщение от Yegor
А я вот узнал, что NModbus не умеет делать несколько слейвов на одном TCP-канале. Печаль-беда.
Странно, мне казалось достаточно изменить адрес слейва в параметрах и в запросе в коде просто указывать нужный слейв (При обращении к слейву первый параметр в команде вроде-как отвечает за адрес слейва).
Но я на практике не проверял (сейчас нет под рукой ПЛК).
Кстати, а подключатся из кода к эмулятору ПЛК запущенному в Codesys нет возможности?
31.05.2016, 15:39
madiggo
Цитата:
Сообщение от Yegor
Есть подозрение, что вы что-то не так делаете. Tell, don't ask. По принципу инкапсуляции, вас не должно беспокоить состояние мастера и уж тем более свойства его транспорта. Вы просто должны быть готовы к тому, что мастер выдаст исключение. Отлавливать исключения проще, чем поддерживать чужое состояние (stateful vs stateless).
Да я то всё делаю так, ловлю исключения. Но ох уж эти мне принципы... На мой взгляд, гораздо проще и безопаснее перед действием проверить, а имеет смысл его совершать? Люди вот под кирпич не ездют, не желая опытным путем проверять, стена там есть или всё-таки нет ;)
14.06.2016, 11:57
fillpackart
Цитата:
Сообщение от Yegor
Попробуйте создавать мастера через ModbusSerialMaster.CreateRtu(TcpClient client) (либо CreateAscii по ситуации). Тогда обмен будет в формате последовательного порта, но канал останется TCP. Это вместо традиционного ModbusIpMaster.CreateIp, где формат сообщений иной.
А как настроить TcpClient? Модем передает данные на фиксированный айпи, сам модем имеет айди, так каким образом я из кода указываю, с каким модемом я соединяюсь?
16.06.2016, 06:48
Yegor
Если данные передаются по инициативе стороны модема и белый айпишник тут, а не там, то о каком мастере на принимающей стороне может идти речь? Делайте наоборот слейв тогда.
20.06.2016, 13:32
fillpackart
Цитата:
Сообщение от Yegor
Если данные передаются по инициативе стороны модема и белый айпишник тут, а не там, то о каком мастере на принимающей стороне может идти речь? Делайте наоборот слейв тогда.
Данные передаются по инициативе опрашивающего ПК.
30.11.2016, 11:03
voale
Цитата:
Сообщение от Yegor
А я вот узнал, что NModbus не умеет делать несколько слейвов на одном TCP-канале. Печаль-беда.
что это значит на одном канале? т.е нельзя использовать например 2устройства плк100(оба slave), которые в одной сети находятся, например с ip 192.168.1.3 и 192.168.1.4? или как это понять?
30.11.2016, 13:32
voale
для решения проблемы " TCP-сокет закрывается через 11-12 секунд бездействия "пытался сделать как в инструкции в NModbus http://ftp.icpdas.com/pub/cd/8000cd/...al_v1.2_en.pdf
на странице 8 : If want to reconnect when offline, refer to following codes.
но это не помогает, может кто скинуть кусок кода кто как делает? заранее огромное спасибо.
30.11.2016, 13:36
voale
Цитата:
Сообщение от capzap
слейвы имеют адреса, если на одном ПК по 502 порту обратится к слейву с номером один и номером два, библиотека не сможет обеспечить работу со вторым слейвом. Вряд ли и другие бибки это делают, ни кто особо не задавался таким вопросом для промышленного применения, максимум для эмуляции
т.е нужно поменять порт на слейве с адресом 2 например(ставить порт 503 вместо 502) ?
и совсем нет решения, когда есть 2 и более слейвов? создавать отдельный tcp канал?
например, так создавать 2 отдельных подключения
слейв 1
master = ModbusIpMaster.CreateIp(new TcpClient("192.168.1.10", 502));
master.WriteSingleCoil(1, 11, true);
может кто знает как связать плк110 и считыватель проксимити карт(hid, e-marine)?
цель такая: как получать считанный код в программе на C# через modbus tcp?
19.06.2017, 17:27
zolex
Вложений: 1
Yegor ,у меня выскакивает сообщение хотя ПЛК настроен правильно!, что может быть в ОС настроено не так? может с правами админа надо файл открывать?как быть с брандмауэром?
21.06.2017, 23:50
voale
Цитата:
Сообщение от greenwod
Важно помнить, что сигнал может быть коротким и Ваша программа может не успеть его считать - я делал дополнительный регистр в котором контролировал сигнал.
как именно контролировал подскажи подробнее?
06.07.2017, 18:34
Yegor
Цитата:
что может быть в ОС настроено не так?
Проект для ПЛК лучше покажите.
02.08.2017, 00:44
S.A.D.
Цитата:
Сообщение от voale
может кто знает как связать плк110 и считыватель проксимити карт(hid, e-marine)?
цель такая: как получать считанный код в программе на C# через modbus tcp?
я делал считывание RFID карт путём выбора считывателя с RS232 интерфейсом и получал код карты в порт через библиотеку SysLibCom в ASCII кодировке.
08.09.2017, 12:06
dimakovalenko93
Подскажите.
По вашому примеру все вышло и работает.
Но мне нужно передавать число типа Real.
Подскажите как ето реализовать.
Спасибо