Ща будем подрубать Овеновский ПЛК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:
Теперь можно написать мастера сети (айпишник своего ПЛК в длинную строчку подставьте):
Код:
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]);
}
}
}
}
Само собой, ModbusIpMaster можно заменить на ModbusSerialMaster, скормить ему вместо TcpClient последовательный порт (SerialPort), и таким образом получить Modbus RTU вместо Modbus TCP.
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). Дальше немножко магии, и у нас есть объект, который сообщает об изменениях тегов и отдаёт эти теги по запросу. Теперь в классе окна (а в серьёзном проекте лучше где-нибудь повыше) мы скармаливаем регистры уже объекту этого класса и задаём привязку контролов на него. Можно не ковыряя лишний раз код добавить текстовое поле только за счёт разметки, например: