March 10, 2016

Асинхронные функции TwinCAT.Ads API

Библиотеки ADS.API поддерживают событийно-ориентированное программирование — вместо постоянного чтения значения переменной (поллинг, polling) можно подписаться на рассылку уведомлений и получать уведомление только при изменении значения переменной. Впрочем, можно составить и постоянный опрос: уведомления будут приходить через интервалы времени, заданные разработчиком.
Можно одновременно использовать механизм уведомлений и обычные синхронные операции.
Инициатива по созданию событий-уведомлений (Device Notification) принадлежит программе созданной на языке высокого уровня (в нашем случае — C#, ПК), но сам механизм уведомлений находится в роутере контроллера. Не рекомендуется создавать более 550 уведомлений для одного устройства, т. к. каждая новая подписка дополнительно нагружает контроллер. Для увеличения нагрузочной способности лучше упаковать или организовать данные в структуры и обмениваться пачками данных. Для снижения нагрузки на клиентское приложение, можно ввести ограничение на частоту возникновения уведомлений, а точнее, на промежуток между отправкой уведомлений.
Всегда приходит первое уведомление. После подписки приемник считает, что значение переменной не определено, поэтому сразу же приходит первое уведомление с актуальным значением переменной.

Использование дополнительных задач


Начнем сразу со сложного: создаем дополнительную задачу (Additional Task), оставляем порт равным 301 по умолчанию, и затем связываем переменные задачи с модулями расширения физических входов/выходов на шине EtherCAT:


Создавать и загружать программу ПЛК-задачи не требуется. В этом основное преимущество фиктивных, дополнительных задач: при минимуме телодвижений, можно сразу же начать читать данные с шины, т. е. с модулей ввода/вывода.


Подписка


Переходим к программе на C#. Для подписки на события используется метод AddDeviceNotification класса TcAdsClient:

TcAdsClient client301;
int h_client301;

AdsStream adsStream;

private void MainForm_Load(object sender, EventArgs e)
{
    client301 = new TcAdsClient();
    client301.Connect("5.2.100.109.1.1", 301);

    adsStream = new AdsStream(32);
 
    try
    {
        h_client301 = clientPlc.AddDeviceNotification(
            "Task 2.Inputs.EL1018",
            adsStream, 0, sizeof(byte),
            AdsTransMode.OnChange, 100, 10000,
            null);
    }
    catch (AdsErrorException adsExc)
    {
        // LOG: adsExс.Message;
        client301.Dispose();
        client301 = null;
    }

    if (client301 != null)
    {
        client301.AdsNotification += Client301_AdsNotification;
    }
    
    [...]

В метод передается имя переменной на изменения которой мы подписываемся. Затем указывается поток входных данных AdsStream. Необходимо создать поток с буфером, размер которого достаточен для размещения значений переменной. Кроме этого, задается смещение в потоке и размер данных переменной в байтах (подробнее — ниже).

AdsTransMode определяет тип опроса переменной. На выбор предоставляется несколько вариантов, но в данный момент реализовано только два. Cyclic — задает периодический опрос значения переменной с интервалом заданным параметром cycleTime (в примере выше, это 100 миллисекунд). OnChange — уведомления приходят только при изменении значения переменной.

Параметр maxDelay задает длительность интервала между двумя уведомлениями. По его истечении друг за другом придут все те уведомления, что произошли в этот промежуток времени, т. е. роутер накапливает их, а затем выдает пулеметной очередью друг за другом.

В последний параметр userData разработчик может передать какие-либо свои данные. Они будут доступны при получении уведомления.


Освобождение ресурсов


Уведомления создаются на стороне контроллера, поэтому по отношению к программе клиенту они являются внешними и соответственно неуправляемыми ресурсами (umanaged resources). По окончании работы лучше освободить их.

private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (client301 != null)
    {
        try
        {
            client301.DeleteDeviceNotification(h_client301);
        }
        catch (AdsErrorException adsExс)
        {
            // LOG: adsExс.Message;
        }
        finally
        {
            client301.Dispose();
            client301 = null;
        }
    }
    
    [...]
 
В принципе, финализаторы библиотеки самостоятельно справятся с освобождением занятого, но во избежании внезапных протечек памяти лучше сделать это самостоятельно.


Уведомления


Когда-нибудь наступит тот самый момент и придет уведомление. Первое, что необходимо сделать при получении, это сдвинуть курсор чтения на N-байт. На сколько его необходимо сдвинуть мы задаем при подписке на уведомления.

private void Client301_AdsNotification(object sender, AdsNotificationEventArgs e)
{
    e.DataStream.Position = e.Offset;
    byte el1008 = (byte)e.DataStream.ReadByte();

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


Чтение данных EtherCAT-шины


Включаем ADS-сервер, который будет предоставлять данные с шины.


Включенный сервер не является потребителем данных. Без задач, читающих данные с шины, вы получите спящую шину и по прежнему неработающий ADS-сервер: Device 'Device 3 (EtherCAT)' needs sync master (at least one variable linked to a task variable). Создайте хотя бы одну дополнительную задачу и слинкуйте хотя бы одну переменную.

Подписываемся аналогично, за исключением номера порта — 27907:

clientECat = new TcAdsClient();
clientECat.Connect("5.2.100.109.1.1", 27907);

h_clientECat = clientECat.AddDeviceNotification(
    "Inputs.Frm0InputToggle",
    adsStream, 1, sizeof(ushort),
    AdsTransMode.OnChange, 10, 0,
    null);


Работа со структурами


TYPE ST_DataPack :
STRUCT
    Index    : UINT;
    Name     : STRING(10);
    Speed    : REAL;
END_STRUCT
END_TYPE

PROGRAM MAIN
VAR
    Pack     : ST_DataPack := (Index := 0, Name := '', Speed := 0);
    state    : UINT;
END_VAR

CASE state OF
0: (* Инициализация *)
    Pack.Index    := 1;
    Pack.Name     := 'LightSpeed';
    Pack.Speed    := 0;

    state := 100;

100: (* Ускорение *)
    Pack.Speed := Pack.Speed + 0.1;
    Pack.Index := REAL_TO_UINT(Pack.Speed / 100);

END_CASE


На стороне клиента объявляется аналогичная структура.
В примере ниже предлагается почленное заполнение полей структуры, поэтому нет необходимости в строгой последовательности полей и применении атрибутов типа StructLayout и т. п.
Следует помнить о несоответствии размеров типов данных TwinCAT и C#, а также правильно вычислить размер буфера для потока. Для строковых переменных необходимо указывать максимальное количество символов (по умолчанию TwinCAT задает его равным 80 символам).

public struct DataPack
{
    public ushort Index;
    public string Name;
    public float Speed;
}

DataPack backpack = new DataPack();

TcAdsClient clientPlc;
int h_clientPlc;

AdsStream adsStream;

private void MainForm_Load(object sender, EventArgs e)
{
    clientPlc = new TcAdsClient();
    clientPlc.Connect("5.2.100.109.1.1", (int)AmsPort.PlcRuntime1);
    adsStream = new AdsStream(32);
 
    try
    {
        h_clientPlc = clientPlc.AddDeviceNotification(
            "MAIN.Pack",
            adsStream, 3, sizeof(ushort) + sizeof(float) + 10,
            AdsTransMode.OnChange, 100, 0,
            null);
    }
    catch (AdsErrorException adsExc)
    {
        // LOG: adsExс.Message;
        clientPlc.Dispose();
        clientPlc = null;
    }

    if (clientPlc != null)
    {
        clientPlc.AdsNotification += ClientPlc_AdsNotification;
    }

    [...]


Для удобства чтения полей структуры создается AdsBinaryStream, который умеет десериализовать в самые распространенные типы данных.

private void ClientPlc_AdsNotification(object sender, AdsNotificationEventArgs e)
{
    e.DataStream.Position = e.Offset;

    var reader = new AdsBinaryReader(e.DataStream);

    backpack.Index = reader.ReadUInt16();
    backpack.Name  = new string(reader.ReadChars(10));
    backpack.Speed = reader.ReadSingle();
}


Запись структуры производится точно так же как и запись обычной переменной.

backpack.Index = 0;
backpack.Name  = "PC written";
backpack.Speed = 300000;

clientPlc.WriteSymbol("MAIN.Pack", backpack, false);

No comments

Post a Comment

Note: Only a member of this blog may post a comment.