May 25, 2015

ActiveX в HMI

В HMI визуализацию можно добавить компонент ActiveX. Если вы не умеете работать с ActiveX компонентами, не знаете C/C++ и не умеете создавать DLL-файлы, можете забыть про эту возможность и просто написать свое собственное приложение, которое будет "выгребать" данные через ADS.API. В общем, именно так и нужно поступить, так как ActiveX морально устарел.

Нюансы вставки ActiveX-компонента:
  • Только пассивный элемент, только отображение.
  • Только настольные операционные системы: Windows XP, 7 и старше.
  • Не работает под Windows CE и другие Сompact версии.
  • Основная цель - вставка компонента отображения HTML-страниц (Microsoft WebBrowser), но можно и другие.



На вкладке Methodcalls сосредоточены настройки вызова различных методов интерфейса ActiveX-компонента:



Webbrowser


Вкладка для компонентов, поддерживающих интерфейс IWebBrowser:
  • Variable for URL - переменная, содержащая строку адреса веб-сайта (URL).
  • Condition for call - старт загрузки html-страницы по переднему фронту TRUE, заданной переменной. Если переменная не задана - компонент будет обновляться каждый цикл ПЛК! Лучше - задать.


Additional Call


Позволяет настроить вызов других методов ActiveX-компонента:
  • Dll for Call - полный путь к DLL-файлу, который в свою очередь будет вызывать какие-то методы компонента.
  • Methodidentification - строка, содержащая название метода, который будет вызываться внутри DLL. Это не само название метода, а подсказка для внутренностей DLL-библиотеки - что необходимо делать (см. пример ниже).
  • Parameter - дополнительные параметры для передачи в DLL-библиотеку.
  • Condition for call - старт вызова по переднему фронту заданной переменной.

DLL-библиотека должна экспортировать функцию ExecuteActiveXCall:

void ExecuteActiveXCall(IUnknown* pUnk, char* pszId, char* pszParam,
char* pszReturnBuffer, int nReturnBufferSize, DWORD* pdwReturnFlag);

  • pszId - строка из переменной Methodidentification.
  • pszParam - строка из переменной Parameter.
  • pUnk - ссылка на интерфейс для работы с методами ActiveX-компонента.
  • pszReturnBuffer, nReturnBufferSize и pdwReturnFlag - не используются.


Пример DLL-библиотеки


Данный пример показывает, как может выглядеть простейшая DLL-библиотека, позволяющая вызывать другие методы ActiveX-компонента. Здесь GoBack и GoForward позволяют переходить на страницу вперед или на страницу назад, при условии, что компонент поддерживает интерфейс IWebBrowser.

Названия IWebBrowser|GoBack и IWebBrowser|GoForward никак не связаны с названиями методов компонента. Разработчик может использовать произвольные, удобные для него названия:

#include "stdafx.h" 
#include <unknwn.h> 
#include <exdisp.h> 
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 
{
    return TRUE; 
}

extern "C" __declspec (dllexport) void ExecuteActiveXCall(IUnknown* pUnk, char* pszId, char* pszParam, char* pszReturnBuffer, int nReturnBufferSize, DWORD* pdwReturnFlag) 
{ 
    if (strcmp(pszId, "IWebBrowser|GoBack") == 0)
    { 
        IUnknown* pNewUnk; 
        IWebBrowser* pwb; 
        pUnk->QueryInterface(IID_IWebBrowser, (void**) &pNewUnk);
        pwb = (IWebBrowser*) pNewUnk;
        if (pwb) 
        { 
            pwb->GoBack(); 
            pwb->Release(); 
        }
    }
    else
    {
        if (strcmp(pszId, "IWebBrowser|GoForward") == 0)
        { 
            IUnknown* pNewUnk; 
            IWebBrowser* pwb; 
            pUnk->QueryInterface(IID_IWebBrowser, (void**) &pNewUnk);
            pwb = (IWebBrowser*) pNewUnk;
            if (pwb) 
            { 
                pwb->GoForward(); 
                pwb->Release(); 
            } 
        } 
    }
}


Функция добавления ActiveX-компонента - это стандартная функция CoDeSys PLC Control; она может использоваться и в контроллерах других производителей.

May 8, 2015

Обмен данными через ADS.API, ST и C#

При работе с ADS.API и.NET необходимо учитывать, что у данных есть длина: причем ПЛК-задачи используют 16-разрядное слово, а C# 32-х. В справочной системе есть краткая таблица соответствия типов. Сделаем ее длинношеее.

STC#Знак.Длина
BYTE, USINTbyte+8 бит
INTshort±16 бит
WORD, UINTushort+16 бит
DINTint±32 бита
DWORD, UDINTuint+32 бита
REALfloat±32 бита
LREALdouble±64 бита

Ничего сложного, для базовых типов нужно учитывать разницу в длине слов. Специфические типы данных DT и TIME требуют особого подхода. STRING и ARRAY - особого подхода с элементами эквилибристики. Работа со структурами STRUCT - тема отдельного разговора.


Дата и время


TIME в языке ST - это промежуток времени (длина), DT - дата и время события (момент). Соответственно в C# для TIME подходит TimeSpan, для DT - DateTime.

TIME хранится в памяти ПЛК как 32-х разрядное, целое, беззнаковое число, содержащее количество миллисекунд:

int htime = client.CreateVariableHandle("MAIN.t");
UInt32 ttime = 0;
ttime = (UInt32)client.ReadAny(htime, ttime.GetType());
TimeSpan span = TimeSpan.FromMilliseconds(ttime);

Я специально использовал тип данных UInt32, чтобы был виден размер данных. По желанию его можно заменить на uint.

С DT немного сложнее, но не сильно - это опять-таки 32-х разрядное, целое, беззнаковое, содержащее количество секунд, прошедшее с 1 января 1970 года:

int hdate = client.CreateVariableHandle("MAIN.td");
int tdate = 0;
tdate = (int)client.ReadAny(hdate, tdate.GetType());
DateTime dt = new DateTime(1970, 1, 1).AddSeconds(tdate);
Console.WriteLine(string.Concat("DT td = ", dt.ToLongDateString(), " / ", dt.ToLongTimeString()));

Я немного расширил пример выводом даты и времени в консоль.


Массивы данных


Сразу отмечу, что строки - это одномерные массивы символов. Нет никакой разницы между работой со строками и массивами.
В старых версиях TwinCAT необходимо было учитывать, что в конце строки стоит символ '\0' и соответственно увеличивать на единицу размер массива под строку. Начиная со второй версии, про нулевой символ и увеличение размера можно забыть: просто передавайте длину строки в символах.
Для чтения и записи массивов используется дополнительный параметр, передающий количество элементов массива. Для одномерного массива - число элементов, для двумерного - число строк, затем число столбцов.

VAR
    str   : STRING      := 'abcdef';
    strT  : T_MaxString := '01234abcdef';
    arr   : ARRAY [1..5] OF WORD;
    arr2D : ARRAY [1..3, 1..5] OF WORD;
END_VAR

string    str   = "";
string    strT  = "";
ushort[]  arr   = new ushort[5];
ushort[,] arr2d = new ushort[3, 5];

str   = (string)   (client.ReadAny(h_str,   str.GetType(),   new int[] { 80   }));
strT  = (string)   (client.ReadAny(h_strT,  strT.GetType(),  new int[] { 255  }));
arr   = (ushort[]) (client.ReadAny(h_arr,   arr.GetType(),   new int[] { 5    }));
arr2d = (ushort[,])(client.ReadAny(h_arr2D, arr2d.GetType(), new int[] { 3, 5 }));


При записи строк - длину указывать необходимо, т. к. можно передавать только часть строки, а вот при записи массивов наоборот - длину передавать не только не нужно, но и нельзя:

client.WriteAny(h_str,   str,   new int[] { str.Length });
client.WriteAny(h_arr,   arr);
client.WriteAny(h_arr2D, arr2d);


Все переменные сразу


Существует возможность прочитать список всех переменных ПЛК-программы: получить исчерпывающую информацию об окружении переменной, ее типе, группе, смещении в памяти, родительской структуре и пр. В терминологии TwinCAT - это называется "Symbol Information". Даже комментарий, оставленный в строке с объявленной переменной, будет доступен через информацию окружения.

// Загружаем информацию о всех доступных переменных
TcAdsSymbolInfoLoader symbolLoader = client.CreateSymbolInfoLoader();
TcAdsSymbolInfoCollection infoCollection = symbolLoader.GetSymbols(true);

foreach (TcAdsSymbolInfo symInfo in infoCollection)
{
    string  name        = symInfo.Name;
    string  shortName   = symInfo.ShortName;
    string  type        = symInfo.Type;
    int     size        = symInfo.Size;
    string  comment     = symInfo.Comment;
    long    iGroup      = symInfo.IndexGroup;
    long    iOffset     = symInfo.IndexOffset;
    
    TcAdsSymbolInfo parentInfo = symInfo.Parent;

    Console.WriteLine(string.Concat(
        name,                   Environment.NewLine,
        shortName,              Environment.NewLine,
        type,                   Environment.NewLine,
        size,                   Environment.NewLine,
        comment,                Environment.NewLine,
        iGroup, ":", iOffset,   Environment.NewLine
        ));
}

Name и ShortName ничем, к сожалению, не отличаются, хотя должны. Поля Parent и xxxSubSymbol (в примере выше его нет) организуют переменные в иерархию.

Подробнее про назначение полей можно прочитать в справочной системе "TcAdsSymbolInfo Members".

May 7, 2015

Как готовить синхронные функции ADS.API

Сборник рецептов как корректно обращаться с базовыми синхронными функциями ADS.API.


Инициализация


PROGRAM MAIN
VAR
    PLCVar : INT;
END_VAR
...
private int hVar;
private TcAdsClient adsClient;
private short plcVar;
...

TcAdsClient adsClient = new TcAdsClient(); 


Подключаемся к ПЛК-рантайму №1.
Для TwinCAT2 порт = 801, для TwinCAT3 порт = 851.

adsClient.Connect("172.16.3.217.1.1", 801); // TwinCAT 2
adsClient.Connect("172.16.3.217.1.1", 851); // TwinCAT 3


Подключение


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

StateInfo info = new StateInfo(AdsState.Invalid, 0);
while (info.AdsState != AdsState.Run)
{
    try
    {
        info = adsClient.ReadState();
    }
    catch { }

    Thread.Sleep(100); // ждем 100мс перед следующей проверкой
}
// OnConnected(); // отправляем в родительский поток сообщение о подключении

Такой кусок кода нужно запустить в отдельном потоке и дождаться сообщения о благополучном запуске задачи. В C#.NET 4.5 удобно использовать конструкции async/await.


Синхронное чтение


Создаем дескриптор для переменной из контроллера

try
{
    hVar = adsClient.CreateVariableHandle("MAIN.PLCVar");
}
catch(Exception ex)
{
    MessageBox.Show(ex.Message);
}


Немного исправим способ из справочной системы.

plcVar = (short) adsClient.ReadAny(hVar, typeof(short));          // Было
plcVar = (short) adsClient.ReadAny(hVar, plcVar.GetType());       // Стало *
plcVar = (short)(adsClient.ReadAny(hVar, plcVar.GetType()) ?? 0); // Избыточный вариант **

(*) typeof(short) лучше превратить в plcVar.GetType(), это заставит разработчика проинициализировать переменную до ее первого использования.
(**) ReadAny() возвращает object который всегда не-null, иначе выбрасывается исключение, т. е. null не бывает никогда, поэтому можно избежать использования nullable-типов (short?) с дальнейшей проверкой на null или более простой конструкции "??".


Обработка ошибок


Так как до- или во время чтения/записи может произойти обрыв связи или другие неприятности, необходима обработка исключений.

try
{
    plcVar = (short)adsClient.ReadAny(hVar, plcVar.GetType());
}
catch {}

Но, здесь нет анализа ошибки.


try
{
    plcVar = (short)adsClient.ReadAny(hVar, plcVar.GetType());
}
catch(Exception ex)
{
    MessageBox.Show(ex.Message);
}

Вместо анализа ошибки, закидает пользователя и рабочий стол сообщениями об ошибке.


AdsErrorCode errCode = AdsErrorCode.NoError;
try
{
    plcVar = (short)adsClient.ReadAny(hVar, plcVar.GetType());
}
catch(AdsErrorException ex)
{
    errCode = ex.ErrorCode;
}
catch(Exception genex)
{
    // Произошло что-то не связанное с ADS.API
}

if (errCode != AdsErrorCode.NoError)
{
    switch(ex.ErrorCode)
    {
        case AdsErrorCode.ClientSyncTimeOut:
            // Обрыв связи
            // OnDisconnected();
            break;
        case AdsErrorCode.DeviceSymbolNotFound:
            // Переменная пропала (например, была стерта программы)
            // OnInvalidData();
            break;
        default:
            // Все, что не требует обработки
            // OnGeneralError();
            break;
    }
}

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