February 4, 2016

Простые функции ADS.API

В ADS.API существуют две функции, которые позволяют читать/писать данные с контроллера буквально одной строкой: ReadSymbol и WriteSymbol. Эти функции сильно упрощают разработку на языках высокого уровня. Как обычно, далее про C#.

Разработка ADS.API двигается по пути упрощения интерфейса библиотеки: от работы с потоками через индексы и смещения (AdsStream), к работе с произвольными типами данных в виде именованных переменных (Read|WriteAny). Имена переменных ПЛК-задачи удобнее, чем абстрактные потоки байт, но по-прежнему необходимо преобразовывать имена в дескрипторы и только затем работать с ними. Напрашивается сделать следующий шаг на пути сокращения.

Symbol в TwinCAT-системе — это описание переменной и вся возможная информация о ней, т. е. что-то очень похожее на отражения или рефлексию в языках высокого уровня. Вообще, работа с переменными в библиотеке ADS.API построена не самым лучшим образом. Если есть желание взглянуть на то, каким оно должно быть или стать в будущем — лучше взгляните на независимый проект adsclient на гитхабе. Когда понравится — перечитайте про преимущество другой стороны ADS.

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

using (TcAdsClient client = new TcAdsClient())
{
    client.Connect("192.168.1.17.1.1", 801);
    
    client.WriteSymbol("MAIN.uint16", (ushort)123, false);
    
    bool    m_bool1   = (bool)   client.ReadSymbol( "MAIN.bool1",   typeof(bool),   false);
    byte    m_byte8   = (byte)   client.ReadSymbol( "MAIN.byte8",   typeof(byte),   false);
    short   m_int16   = (short)  client.ReadSymbol( "MAIN.int16",   typeof(short),  false);
    ushort  m_uint16  = (ushort) client.ReadSymbol( "MAIN.uint16",  typeof(ushort), false);
    ushort  m_word16  = (ushort) client.ReadSymbol( "MAIN.word16",  typeof(ushort), false);
    float   m_real4b  = (float)  client.ReadSymbol( "MAIN.real4b",  typeof(float),  false);
    double  m_lreal8b = (double) client.ReadSymbol( "MAIN.lreal8b", typeof(double), false);
}


Аналогично для "системных" типов данных:

client.WriteSymbol("MAIN.word16", (UInt16)456, false);

Int16  s_int16   = (Int16)  client.ReadSymbol( "MAIN.int16",   typeof(Int16),  false);
UInt16 s_uint16  = (UInt16) client.ReadSymbol( "MAIN.uint16",  typeof(UInt16), false);
UInt16 s_word16  = (UInt16) client.ReadSymbol( "MAIN.word16",  typeof(UInt16), false);
Single s_real4b  = (Single) client.ReadSymbol( "MAIN.real4b",  typeof(Single), false);
Double s_lreal8b = (Double) client.ReadSymbol( "MAIN.lreal8b", typeof(Double), false);


Обратная разработка


Упрощения не даются просто так и где-то что-то приходится приносить в жертву. Простые в использовании функции могут работать как-то не так или просто "тормозить". Заглянем во внутренности библиотеки, а конкретнее — в TcAdsClient. С небольшими сокращениями функции выглядят так:

public object ReadSymbol(string name, Type type, bool reloadSymbolInfo)
{
    [...]

    if (this._symbolInfoTable == null)
        this._symbolInfoTable = new SymbolInfoTable(this);

    return this._symbolInfoTable.ReadSymbol(name, type, reloadSymbolInfo);
}


public void WriteSymbol(string name, object value, bool reloadSymbolInfo)
{
    [...]

    if (this._symbolInfoTable == null)
        this._symbolInfoTable = new SymbolInfoTable(this);

    this._symbolInfoTable.WriteSymbol(name, value, reloadSymbolInfo);
}

Видно, что это обычные обертки создающие объект некой таблицы символьной информации, а затем вызывающие одноименные методы этой таблицы. На ум сразу приходит идея о кешировании дескрипторов и пр. Смотрим дальше:

namespace TwinCAT.Ads.Internal
{
    internal class SymbolInfoTable
    {
        private Dictionary<string, TcAdsSymbol> _symbolTable;
        private DatatypeInfoTable _datatypeTable;
        private TcAdsClient _adsClient;
        
        public SymbolInfoTable(TcAdsClient adsClient)
        {
            this._adsClient = adsClient;
            this._symbolTable = new Dictionary<string, TcAdsSymbol>((IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
            this._datatypeTable = new DatatypeInfoTable(adsClient);
        }

И да, действительно, это обертки для кеширования информации о переменных: словарь имен и таблиц символов, коллекция типов данных и обратная ссылка на владельца — класс AdsClient. Делаем вывод — данная таблица создается при первом вызове одной из функций ads-клиента и хранит информацию о переменных, что сокращает время и количество операций при работе с переменными ПЛК-задачи.


Третий параметр


Вернемся к "reloadSymbolInfo", третьему параметру функций Read|WriteSymbol. Он позволяет сбросить, стереть кешированную символьную информацию, накопленную в таблицах объекта класса SymbolInfoTable. Это хорошо видно в реализации методов класса (в начале каждого метода параметр проверяется и если требуется — таблица очищается):

public void WriteSymbol(string name, object value, bool bReloadInfo)
{
    if (bReloadInfo)
        this.Cleanup();
    [...]


public object ReadSymbol(string name, Type type, bool bReloadInfo)
{
    if (bReloadInfo)
        this.Cleanup();
    [...]

Зачем нужно чистить таблицы, если они созданы для кеширования данных и ускорения работы? Для того, чтобы в случае устаревания этих данных, мы могли их сбросить. Устареть информация может при перезагрузке контроллера или при использовании одной и той же программы с различными контроллерами. Остальное додумайте самостоятельно.


Передача данных


Осталось выяснить как передаются данные. Продолжаем рассматривать методы Read|WriteSymbol класса SymbolInfoTable, пропускаем сброс таблиц (3):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void WriteSymbol(string name, object value, bool bReloadInfo)
{
    [...]
    TcAdsSymbol symbol = this.GetSymbol(name, true);
    if (symbol == null)
        TcAdsDllWrapper.ThrowAdsException(AdsErrorCode.DeviceSymbolNotFound);

    TcAdsDatatype dataype = this._datatypeTable.GetDataype(symbol.Type);
    if (dataype == null)
        throw new NotSupportedException("Type of symbol not supported");

    AdsStream stream = new AdsStream((int) dataype.size);
    AdsBinaryWriter writer = new AdsBinaryWriter(stream);

    if (dataype.IsPrimitive)
        this.SetPrimitiveType(symbol.Name, value, dataype.managedType, symbol.Datatype, symbol.Size, writer);

В строках 4-6 идет выборка кешированной информации, затем (строки 8-10) выборка типов переменных и наконец подготовка потоков (строки 12-13, AdsStream, AdsBinaryWriter). Никакой магии.

В строках 15-16 и далее, идет запись данных через подготовленные потоки, но это уже другая история.


Выводы:
  • Символьная информация кешируется, а дескрипторы создаются и уничтожаются незаметно для разработчика.
  • Если что-то случится, таблицы можно сбросить.
  • Передача данных по-прежнему ведется через потоки, т. е. быстро.
  • Функции удобны, просты и нужно использовать.

No comments

Post a Comment

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