November 7, 2019

Subrange Types

Infosys it at Subrange Types or it looks like INT(1..42);

You can bound a variable of the integer type into the predefined range. In other words you can set the low limit and the hi limit for the integer variable. Thus you can create a new integer type. But is it realy? 

Actual for the following types only: SINT, USINT, INT, UINT, DINT, UDINT, BYTE, WORD, DWORD, LINT, ULINT, LWORD.

Let's play in the sandbox. For the clarity, I have used underscores in the names of subrange variables.

PROGRAM MAIN
VAR
    i: DINT;
    value: INT;
    _value_: INT(10..90);
    analog: REAL;

    pValue : PVOID;

You can do same actions with a variable of subrange type as with a variable of an integer type. In fact, this is not a new data type with a new bounds (or restrictions), but it is a suggestion or a clue to the compiler how to check some cases (I will discuss it later) or how to control variable value.
Unfortunately, you cannot do this: _analog_ : REAL (4.0 .. 20.0); and it is not about too many dots in a row but because of the inaccuracy of the floating-point types.

Truth Or Dare


Possible:

_value_ := 42;
_value_ := value;

_value_ := _value_ + 1;
_value_ := _value_ + 100; // O_o

_value_ := DINT_TO_INT(i);
_value_ := REAL_TO_INT(analog);

Are not allowed:

_value_ := 0;
FOR _value_ := 11 TO 89 DO


Excavating Memory


So I decided to check whether or not someone controls the output of the variable value for the specified range during the operation process. I wrote zero to the variable `value` through the pointers. And zero was recorded (nothing bad happened). And then I have added 1 to the variable and suddenly, the value of the variable increased by 1. Where is the bounds? 

// _value_ = 10;

value := 0;
pValue := ADR(_value_);
MEMCPY(pValue, ADR(value), SIZEOF(_value_));
// _value_ = 0;

_value_ := _value_ + 1;
// _value_ = 1;

In general, if we performe cyclically _value_ := _value_ + 1; we will see the whole range of INT values from -32768 to +32767, without any restrictions from the runtime system.

Conclusion: the runtime does not control the value of the variable; it works like a regular integer type, and nothing else.


Implicit Checks


There is a whole family of POUs for implicit checks of the variable value (e.g. division by zero checks POUs):



You have to add some special POUs to the project to control data bounds during the execution:



...then choose L|Range Checks and finally rebuild project. Newcome POUs will control the variable and limits it if the value overstep the bounds defined by you. And now if you repeat the last used trick with pointers but with already activated implicit checking you can get completely different result. Runtime system will call corresponding POU that will limit variable to the predefined bounds.


Performance


FYI, performance measurements are performed to catch whether additional work is happening behind the scenes or not. There was no point in optimizing for 1-2 nanoseconds, although who knows what's going on there. Vertical axis in hundreds of nanoseconds: 83000 = 8.3 milliseconds.

Testing ground:

// HOWMUCH: DINT := 1_000_000;

CASE state OF
1,5:; // empty loop

2: // increment
    FOR i := 0 TO HOWMUCH DO
        value := value + 1;
    END_FOR

3: // const in bounds
    FOR i := 0 TO HOWMUCH DO
        _value_ := 42;
    END_FOR

4: // increment that overstep the bounds
    FOR i := 0 TO HOWMUCH DO
        _value_ := _value_ + 1;
    END_FOR


Without POUs for implicit checks:

Added POUs for implicit checks then the project was rebuilt:

Conclusion: POUs for implicit checks cause extra load.


Resume

  • In fact, this is not a new data type, just an ordinary integer type (INTBYTE, ...) with extra information about subrange.
  • Range are clue for a compiler.
  • Range controlled by extra POUs for implicit checks. This is optional.
  • Implicit checks cause extra load because of execution extra POUs.

Типы с диапазоном

Или Subrange Types типа INT(1..42);

Переменную целого типа можно вписать в заранее ограниченный диапазон. Иначе говоря, ограничить ее значение сверху и снизу. Работает только для переменных целого типа: SINT, USINT, INT, UINT, DINT, UDINT, BYTE, WORD, DWORD, LINT, ULINT, LWORD.

Поиграем в такой песочнице:

PROGRAM MAIN
VAR
    i: DINT;
    value: INT;
    _value_: INT(10..90);
    analog: REAL;

    pValue : PVOID;

Для красоты я обрамил переменную с диапазоном символами подчеркивания. С переменного такого типа можно делать все то же самое, что и с переменными целого типа. По сути это не отдельный тип данных с ограничением, а предписание компилятору по сборке кода и в некоторых случаях (расскажу позже) по контролю за значением переменной.
К сожалению нельзя сделать так: _analog_ : REAL (4.0 .. 20.0); и не потому, что точки мешают, а по причине неточности типов с плавающей точкой.

Примеряем


Что можно делать:

_value_ := 42;
_value_ := value;

_value_ := _value_ + 1;
_value_ := _value_ + 100; // O_o

_value_ := DINT_TO_INT(i);
_value_ := REAL_TO_INT(analog);

Что компилятор не пропустит:

_value_ := 0;
FOR _value_ := 11 TO 89 DO


Играем с указателями


Я решил проверить контролирует ли кто-нибудь во время работы выход значения переменной за указанный диапазон. И через указатель записал ноль в переменную value. И ноль записался (ничего страшного не произошло). А затем я прибавил к переменной единицу и... значение переменной увеличилось на 1:

// _value_ = 10;

value := 0;
pValue := ADR(_value_);
MEMCPY(pValue, ADR(value), SIZEOF(_value_));
// _value_ = 0;

_value_ := _value_ + 1;
// _value_ = 1;

Вообще, если циклически выполнять _value_ := _value_ + 1; то мы увидим весь диапазон значений INT от -32768 до +32767. Без каких-либо ограничений.

Вывод: среда исполнения не контролирует значение переменной, для нее это обычный целочисленный тип.


Неявный контроль времени исполнения


Или `implicit checks`. Это целое семейство ФБ неявного контроля значений переменных (например, ФБ контролирующие деление на ноль):



Чтобы контроль работал во время исполнения программы необходимо добавить в проект функциональные блоки специального назначения:



...выбрать L|Range Checks и пересобрать проект. Добавленные ФБ будут контролировать значение переменной и корректировать его, если значение выйдет за пределы. Если теперь повторно провернуть трюк с указателями, но уже с включенной проверкой, то при следующем обращении к переменной, среда исполнения через соответствующие ФБ исправит/впишет значение переменной в указанный при объявлении диапазон.


Перформанс


Для тех кто не в курсе, измерения производительности выполняются с целью уловить происходит ли дополнительная работа за кулисами или нет. Никакого смысла в оптимизации на 1-2 наносекундах не было и нет, хотя кто знает, что у вас там творится. По вертикали время в сотнях наносекунд: 83000 = 8.3 миллисекунды.

Испытательный полигон:

// HOWMUCH: DINT := 1_000_000;

CASE state OF
1,5:; // пустой цикл

2: // инкремент
    FOR i := 0 TO HOWMUCH DO
        value := value + 1;
    END_FOR

3: // константа, в диапазоне
    FOR i := 0 TO HOWMUCH DO
        _value_ := 42;
    END_FOR

4: // инкремент, выход из диапазона
    FOR i := 0 TO HOWMUCH DO
        _value_ := _value_ + 1;
    END_FOR


Итак, без контроля:


Добавляю контрольные ФБ и пересобираю проект:

Контролируемый выход за пределы диапазона требует дополнительной работы.


Вывод

  • Тип по сути остается обычным целочисленным типом (INT, BYTE, ...).
  • Диапазон — это рекомендация для компилятора.
  • Диапазон учитывается в рантайме только при наличии специальных ФБ, скрыто контролирующих и исправляющих значение переменной.
  • Неявный контроль требует немного дополнительной работы, что логично, так как коррекция значений требует дополнительного вызова ФБ.

November 6, 2019

Производительность ReadAny и Reactive в 5.0.0-preview1

В продолжении темы измерения скорости чтения, посмотрим что там с превью версией под .NET Core 3. Заодно увидим как обстоят дела с реактивностью.


ReadAny


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();
    var connection = session.Connection;

    const int HOWMUCH = 1000;
    List results = new List();

    uint hMain_CycleCount = connection.CreateVariableHandle("MAIN.CycleCount");

    for (int i = 0; i < HOWMUCH; i++)
        results.Add(
            (uint)connection.ReadAny(hMain_CycleCount, typeof(uint)));

    foreach (var cycleNumber in results)
        Console.WriteLine(cycleNumber);
}


.NET Core 3, AdsRouterConsole



Для сравнения повтор картинки из предыдущего поста для .NET 4.7.1, TwinCAT.IO Router



Reactive


static void Main(string[] args)
{
    AdsSession session = new AdsSession(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
    session.Connect();

    const int HOWMUCH = 1000;

    List<uint> results = new List<uint>();

    var valueObserver = Observer.Create(
        val => { // on next
            results.Add(val);
            //Console.WriteLine(val);
        },

        exc => { // on error
            Console.WriteLine(exc.Message);
        },

        () => {  //on completed
            Console.WriteLine($"Read {results.Count} cycles");

            foreach (var cycleNumber in results)
                Console.WriteLine(cycleNumber);
        });

    var subscription = session.Connection
        .WhenNotification("MAIN.CycleCount", NotificationSettings.ImmediatelyOnChange)
        .Take(HOWMUCH)
        .Subscribe(valueObserver);

    Console.WriteLine("ENTER to stop...");
    Console.ReadLine();
}


Все совсем плохо:



Отрицательные значения показывают, что пакеты прилетали не по порядку, а как-то иначе. Причем, если убрать условие в получении N-значений .Take(1000), и просто попытаться выбрать требуемое количество, то получится такая же точно каша с непоследовательными элементами. Что нарушает уверенность в целостности системы.

При времени цикла меньше одной секунды, перестает работать делегат отвечающий за onComplеte часть обозревателя. Именно поэтому пришлось добавить в onNext вывод промежуточных результатов.

Пока что очень сыро и сильно напрягает непоследовательность приходящих элементов.

November 5, 2019

InvokeRpcMethod — вызов удаленного метода

С помощью ADS можно не только передавать данные, но и вызывать методы ПЛК задачи.

TcAdsClient client = new TcAdsClient();
client.Connect(new AmsAddress("5.28.214.97.1.1", AmsPort.R0_RTS + 1));
short result = -1;
var args = new object[]{ result, (short)1, (short)2 };
try {
    result = (short)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
}
catch { }


PROGRAM / ACTION


Проверим странное...
...оно не работает. Нет такого символа
Value cannot be null.
Parameter name: symbol
Работоспособность не зависит от размещения атрибута: что внутри экшена, что над объявлением программы, что и там, и здесь. Результат отрицательный.


FB


Action

Опять нестандартное. И оно тоже не работает.
The RPC method 'A_Func01' is not supported on symbol 'MAIN.fbBox!
Кстати, опечатка в сообщении эксепшена — не хватает кавычки.


Method

Для метода, ожидаемо, работает. Причем, внезапно, вызов метода работает даже когда Runtime в состоянии Stop. Иначе говоря, когда порт создан (851), но ПЛК задача еще не запущена.

Команда __NEW при RPC вызовах также работает, даже в состоянии "стоп", и по прежнему выжирает память роутера. Контроль памяти роутера описан в New, Delete и память роутера. Поэтому если память выделяется внутри RPC метода, а освобождается в другом месте — получится Ахтунг и протечка памяти!

Итоги:
  • Уровень доступа метода не влияет на доступность извне. Любой из PUBLIC, PRIVATE, PROTECTED, INTERNAL доступен для внешнего вызова.
  • Вызов требует наличия всех параметров объявленных в ФБ как VAR_INPUT
  • Точки останова внутри методов не срабатывают при RPC вызовах.


Насколько быстро?


Тестировать будем на CX9020, x32, TwinCAT 3.1.4022.25 и на десктопе Core i7-3630QM x64 TwinCAT 3.1.4024.0. Для интереса сравним производительность относительно обычного чтения символа.

results[i] = (uint)client.InvokeRpcMethod("MAIN.fbBox", "M_FuncPub", args);
// VS
results[i] = (uint)client.ReadSymbol("MAIN.CycleCount", typeof(uint), false);


Будем последовательно и синхронно читать по 1000 значений за раз. В случае с InvokeRpcMethod соответственно вызывать метод и получать результат. Результатом же чтения будет номер текущего цикла ПЛК задачи. При последовательном и многократном чтении, получим массив номеров циклов. Сравнивая два соседних значения и умножая разницу на время цикла ПЛК задачи, получим время затраченное на чтение.

Начну с обычного чтения ReadSymbol. По горизонтали — номер выборки. По вертикали — время в миллисекундах, затраченное на чтение символа. Цветом обозначено время цикла ПЛК-задачи. Первым будет CX9020, вторая картинка — десктоп:

ReadSymbol, CX9020



ReadSymbol, десктоп Core i7 x64


Даже "на глаз" видно, что десктоп справляется "стабильнее" и быстрее. К тому же, в случае десктопа все происходит на локалхосте и нет сетевых прослоек, привносящих дополнительные лаги. Краткий вывод: для циклов от 4 миллисекунд данные возвращаются стабильно. Меньше 4мс могут быть задержки (видимо запросы попадают на границу цикла).

Кроме этого есть запросы с нулевым временем выполнения. Это озорует роутер. При отправке запросов чаще, чем время одного цикла, роутер делает вид, что значение еще не изменилось и быстро шлет обратный ответ с тем же, старым значением. Ну и отлично.

Теперь про RPC запросы. Порядок контроллеров прежний:

RPC, CX9020



RPC, десктоп Core i7 x64


RPC выполняется дольше чем запрос данных одной переменной. Это ожидаемо, так как нужно принять запрос с аргументами, выполнить код, и только затем отправить ответ. 3-4-5 циклов, в зависимости от того, куда упадет запрос относительно границы кванта системного времени.

Правило 4мс по прежнему работает.


Выводы


RPC-запрос может легко выполнить код, который нарушит экосистему рантайма. Например, может вызвать утечку памяти и т. п. В то же время можно попытаться организовать автоматическую систему юнит-тестирования, так как RPC вызовы работают даже на приостановленном рантайме.

Если же необходимо реализовать контурные режимы или передавать данные чаще чем раз в 4мс, то необходимо реализовать промежуточный буфер и отправлять несколько значений за раз, используя обычные функции для работы с переменными ReadAny, WriteSymbol, etc.


PS: Инструменты


Для проведения тестирования и рисования результатов, иногда достаточно обычного экселя (Excel), что и было использовано:




October 24, 2019

Ручной тормоз AM8000

У моторов AM8000 есть опция в виде тормоза. Наличие тормоза указывает единичка в заказном номере мотора AM8000-xxx1-... (и ноль когда тормоз отсутствует). Тормоз управляется отдельной цепью 24 В и срабатывает (накладывается) автоматически при отключении модуляции: тупо прижимается пружиной. И наоборот, при работе оттягивается электромагнитом, освобождая вал двигателя. Намертво тормоз вал не вклинит, но аварийно затормозит (это чтоб не порвало всё к ... и не намотало железо на вал).

В новой прошивке сервоусилителей версии 2.10 появилась возможность подключить внешний тормоз к двигателю без тормоза. Включается через параметр P-0-0060 = 2 (External motor brake: currentless locked).

Редко-редко, а иногда и когда-как, тормоз нужно отключить почти вручную, то есть программно. Управление производится через параметр P-0-0060. Подробнее можно прочитать в Configuration and control of the motor brake, но я туда не полезу, а воспользуюсь удобным функциональным блоком FB_SoEAX5000SetMotorCtrlWord.
Внимание! Не стойте, и не прыгайте, не пойте, не пляшите там, где идет строительство, или подвешен груз. Ручная работа с тормозом чревата отрыванием конечностей и зажевыванием туловища механическими руками. Повязывайте банданы, носите бронекепки и соблюдайте ТБ.
Для работы с ФБ необходимы библиотеки Tc2_MC2 и Tc2_MC2_Drive. В остальном все просто, кроме тристабильного состояния тормоза: вручную наложен, вручную разблокирован и автомат (как повезет):

PROGRAM MAIN
VAR
    {attribute 'TcNcAxis' := 'Axis 1'}
    axis1: AXIS_REF;
    
    EN, SET, LOCK, UNLOCK: BOOL;
    
    mcSetMotorCtrlWord: FB_SoEAX5000SetMotorCtrlWord;
    mcPower: MC_Power;
END_VAR

mcSetMotorCtrlWord(
    Axis := axis1,
    Execute := SET,
    ForceLock := LOCK,
    ForceUnlock := UNLOCK
);

SET R= NOT mcSetMotorCtrlWord.Busy;

mcPower(
    Axis := axis1, 
    Enable := EN, 
    Enable_Positive := TRUE, 
    Enable_Negative := TRUE
);

Для работы с тремя состояними выделено два управляющих бита: ForceLock и ForceUnlock. Сочетанием этих флагов устанавливается текущее состояние тормоза. Вброс значений производится по переднему фронту входа Execute. Я свел все возможные значения в табличку:


Например, возврат тормоза в автоматический режим осуществляется одновременным сбросом флагов ForceLock|Unlock в FALSE и последующей установкой Execute в TRUE. После этого состояние тормоза будет определяться или наличием модуляции, или на уровне NC состоянием флага разрешения Enable в MC_Power, что, впрочем, одно и то же.

Если сервопривод обесточить, то при следующем включении режим тормоза автоматически возвращается к автоматическому. Это поведение по умолчанию. Поэтому еще точнее — его режим после включения определяется значением параметра P-0-0060 в стартап листе. В параметр P-0-0072 транслируются параметры тормоза из мотор-пула двигателей. Значение параметра P-72 можно перезаписать и заменить на другие, но свойства тормоза от этого не изменятся: параметры нужны только для автоматических расчетов внутри программы.

October 21, 2019

Linux + TwinCAT 3 + .Net Core 3 + ADS 5

Система реального времени по прежнему работает под Windows, но реализовать клиентскую сторону, визуализацию или массивную обработку данных (дата саенс, ага) под Linux уже можно.

Не то чтобы раньше нельзя было. Например, родные Бекхоффские реализации протокола ADS на C++, с неродным интерфейсом на QT. Python+Ads для обработки данных, например в блокнотах Jupiter и AdsClient (.NET Standart) для C# под гуем из WPF. Протокол открытый, почему бы и нет?

Теперь же Бекхофф выкатил в публичное превью библиотеки для работы с ADS под .NET Core 3. В первую очередь это означает кросс-платформенность для приложений на C# под Linux, Windows, Mac. О последнем я говорить не хочу, а для первых двух вырисовывается полный стек интересных технологий (или интересный стек полных технологий):
  • .NET Core 3.0
  • AvaloniaUI
  • Beckhoff.TwinCAT.Ads 5

Я еще раз подчеркну, сразу для двух операционных систем: Windows - Linux. Одно и то же приложение. Возможно необходимо пересобрать, тыкнув мышой в выбор операционки, но переписывать код уже не нужно. Выглядеть интерфейс будет одинаково на обеих операционках. Это благодаря Avalonia, которая почти как WPF.


TwinCAT.Ads 5.0.0-preview1


Итак, не далее как неделю назад, Бекхофф выложила превью библиотек. Нас ждут интриги, расследования и гонки на багги. "This is a prerelease version of Beckhoff.TwinCAT.Ads", — говорят нам RaHe, sveno, MichaelKn и Beckhoff со страниц NuGet. Далее идут инструкции как со всем этим жить, но это не точно. Потому что инструкции не работают. Как "нада жить" будет позже, а сейчас про состав выложенного:
  • TwinCAT.Ads 5.0.0-preview1
    Сборка для доступа к данным ПЛК через ADS. Совместима с предыдущими версиями библиотеки. Состав прежний: Client, Session, Read-Write Any Symbol, и тому подобное. Появились асинхронные операции. TcAdsClient переименован в AdsClient; AdsStream заменен на модные и современные Span+Memory.
  • Ads.AdsRouterConsole
    Под Linux нет рантайма и ADS-роутера соответственно. Поэтому Бекхофф предлагает нам нечто вроде выполняющее функции роутера. Запускается как консоль отдельно и самостоятельно, в том числе и под Linux.
  • Ads.TcpRouter
    А это уже не самостоятельная штука, зато можно засунуть себе в программу. Это как бы переносной роутер, который будет внутри вашей программы. Вокруг этой же штуки построена консоль AdsRouterConsole.
  • Ads.Reactive
    Тоже сборка для доступа к данным, но доступ "реактивным" способом.
  • AdsServerAbstractions
    Сервер и всё что сборки делят между собой. Про сервер как-нибудь в другой раз: где еще делать сервер как не под Linux?


Запускаем AdsRouterConsole


Сборка консоли роутера будет лежать в 
%USERPROFILE%\.nuget\packages\beckhoff.twincat.ads.adsrouterconsole\5.0.0-preview1\lib\netcoreapp3.0\TwinCAT.Ads.AdsRouterConsole.dll

...но(!) в инструкции, которая лежит в разделе Documentation NuGet, есть опечатка. На самом деле запускать нужно так:
dotnet TwinCAT.Ads.AdsRouterConsole.dll

...но(!) опять-таки  не запустится, так как этой сборке для запуска не хватает других сборок. Поэтому мы установим сборку TwinCAT.Ads.AdsRouterConsole в свой проект, и тогда .dll файл сборки будет лежать в папке /bin/Debug/netcoreapp3/, а в ней уже автоматом будут все необходимые для запуска консоли роутера сборки.

...но(!) при запуске мы получим следующую ошибку:
A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in '...\bin\Debug\netcoreapp3\'.
Failed to run as a self-contained app. If this should be a framework-dependent app, add the ...\TwinCAT.Ads.AdsRouterConsole.runtimeconfig.json file specifying the appropriate framework.
Это связано с безопасностью в системе, поэтому возвращаемся к
%USERPROFILE%\.nuget\packages\beckhoff.twincat.ads.adsrouterconsole\5.0.0-preview1\lib\netcoreapp3.0
(да-да, именно для этого он нам и нужен был) и кладем файл TwinCAT.Ads.AdsRouterConsole.runtimeconfig.json рядом со сборкой консоли роутера, то есть в каталог /bin/Debug/netcoreapp3/ или /bin/Release/netcoreapp3/ проекта. Выбор конечного пути зависит от этапа разработки и конфигурации проекта.

Файл можно создать самостоятельно, вот его содержимое:

{
    "runtimeOptions": {
        "tfm": "netcoreapp3.0",
        "framework": {
            "name": "Microsoft.NETCore.App",
            "version": "3.0.0"
        }
    }
}

После этого, может запуститься, а может не запуститься и вылететь с ошибкой:
Error: An attempt was made to access a socket in a way forbidden by its access permissions.
...и это всё потому, что у вас уже запущен роутер. Приложение кросплатформенное, можно запускать как под Linux так и под Windows. Прибить работающий роутер можно остановив сервис TcSysSrv из командной строки PowerShell от имени Администратора или через Computer Management.


Таблица роутинга


На самом деле и сейчас не запустится: любому роутеру нужны настройки роутинга. Со стороны ПЛК придется настроить самостоятельно и вручную, а со стороны ПК сформировать XML файл StaticRoutes.xml, расположив его рядом со сборкой консоли роутера:

<?xml version="1.0" encoding="utf-8"?>
<TcConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="C:\TwinCAT3\Config\TcConfig.xsd">
  <Local>
      <Name>RALFH03</Name>
      <NetId>172.17.60.215.1.1</NetId>
  </Local>
  <RemoteConnections>
    <Route>
      <Name>CX-1CD661</Name>
      <Address>169.254.116.30</Address>
      <NetId>5.28.214.97.1.1</NetId>
      <Type>TCP_IP</Type>
    </Route>
  </RemoteConnections>
</TcConfig>

Секция Local — это имя и локальный Ams NetId адрес ПК. Выбираются произвольно, но в роутерах с обеих сторон, в записях касающихся ПК, этот адрес должен совпадать с обеих сторон, то есть во всех роутерах сразу. RemoteConnections — это про удаленную сторону, то есть о ПЛК. Сравним с настройками роутера на ПЛК.

Зайдем на ПЛК через web-конфигуратор: http://169.254.112.234/Config. Далее: TwinCAT → Connectivity → Add TwinCAT Route. Внизу списка жмем кнопки [+] и создаем запись роутинг для удаленного ПК (или ноутбука разработчика), удаленного теперь уже относительно ПЛК:

Route Name: RALFH03
AMS Net ID: 172.17.60.215.1.1
Transport Type: TCP/IP
Address: 169.254.7.118
Connection Timeout (ms): 3000

Сравните "тут" и "там" и станет немного понятнее.


Virtual Box


Я тестировал под виртуальной машиной на x64 Ubuntu 19.04. Для Virtual Box необходимо... краткое объяснение на пальцах — обязательно выставить все три сетевые параметра сетевого коннекта Убунты: IP адрес, маску и обязательно gateway (гейтвей можно произвольный, я выставлял 192.168.0.1). Не забыть установить исполняемому файлу атрибут исполняемого фалйа: chmod +x имя-исп-файла. Установить в системе dotnet core 3 (читайте справку Майкрософта).

И наконец прибить роутер TwinCAT в операционной системе хосте (Windows), иначе будут заняты сетевые порты. Переключения в режим конфигурации (синий) недостаточно, нужен серый значок полного аута.


Багги


А дальше начинаются скачки по дюнам и прочим неровностям превью бета версии библиотек. На любых функциях типа client.ReadSymbol("MAIN.Counter") я получал ошибку:
Stack overflow.
Aborted (core dumped)
...и приложение падает. Интересно, что точно такую же ошибку я получал когда пытался использовать независимую сборку независимого разработчика AdsClient. Видимо, корень проблемы одинаковый.

В то же время можно создать хэндл client.CreateVarHandle("MAIN.Counter"), а затем нормально работать на функциях чтения-записи через хэндлы. Также нормально работает чтение по имени переменной через реактивную подписку, которая к тому же неплохо ложится на реактивность Avalonia:

var valueObserver = Observer.Create(val => {
    Dispatcher.UIThread.InvokeAsync(() => {
        var mwvm = (MainWindowViewModel)DataContext;
        mwvm.Counter = val.ToString();
    });
});

IDisposable subscription =
    session.Connection
        .WhenNotification("MAIN.Counter", NotificationSettings.Default)
        .Subscribe(valueObserver);


То есть как-то работать уже можно и что-то рабочее уже вырисовывается:



Среда разработки


Теперь попробуем поработать под Linux — посмотрим в сторону среды разработки.

Как таковой среды еще нет, зато есть кроссплатформенный редактор Visual Studio Code, а к нему расширение vscode-st за авторством Сергея Романова. Расширение помогает работать с кодом на ST. Проект открытый, исходный код есть на GitHub. Самое главное, что проект не заброшен и развивается: я добавил туда пару новых штук, и внезапно разработчик принял пул-реквест, и так же мгновенно пересобрал расширение.


Подсветка синтаксиса, подсказка по переменным и ФБ, снипеты и прочие плюшки. Еще раз: это не отладчик и не среда разработки, а редактор кода, но это уже что-то. Остается дождаться чего-нибудь подобного TcXaeMgmt, ведь PowerShell Core уже работает под Linux, а роутероподобное ПО мы только что потрогали палочкой. Да что там, TcXaeMgmt уже можно установить, но работать командлеты не будут: отсутствует TwinCAT Automation Interface который построен на COM объектах.

Поэтому — ждем. Редактор файлов уже есть, остается прослойка для работы с загрузкой-выгрузкой файлов-проектов и пуск-стопом рантаймов, а там и до дебага недалеко (на самом деле, далеко).

October 14, 2019

Git и макросы TwinCAT 2 (CoDeSys)

У нас тут настолько TwinCAT 3.1, что некоторые молодые специалисты уже не видели второй версии. Что не отменяет сопровождение старых проектов и желание использовать контроль версий. К тому же есть родня в лице CoDeSys, где все также, но немного по другому.

TwinCAT 2 и Git плохо совместимы из-за бинарного формата проектов TwinCAT 2: использовать можно, но то же diff так просто не получится. Еще есть ENI (ENginering Interface), но оно стоит денег, хотя и не понятно почему? Вероятно, потому что нужен, но если что, есть бесплатное на python, но я не проверял. К тому же оно для Visual SourceSafe и SVN, а нужен Git.

Кому не нужна история или просто лень читать, может сразу пройти на гитхаб — там выложены результаты в виде проекта Tc2_Git.


Что такое, как и почему Git?


Читать можно в произвольном порядке.
  1. Understanding Git (как устроен и работает Git):
  2. Сходство и различие между Mercurial и Git. Ответ на вопрос: почему именно Git?
  3. Git: наглядная справка.
  4. Про Git. Книга и полное описание работы с Git.
  5. Git How To. Интерактивный тур.


Экспорт


Первое, что необходимо сделать, это избавиться от бинарного формата проекта. В PLC Control есть возможность работать из командной строки. Оттуда же можно экспортировать проект из единого бинарного .pro файла в набор текстовых .exp файлов.

Про командную строку и командные файлы PLC Control можно прочитать в Command Line/Command File Commands. Интересно, что экспортировать можно как в один текстовый файл (project export), так и в несколько (project expmul): тогда в каждом, отдельном файле будет лежать отдельный модуль проекта. Разделение модулей по нескольким файлам в дальнейшем облегчит контроль, сравнение и откат изменений в проекте.

Кроме непосредственно экспорта понадобится еще ряд команд, поэтому без командных файлов не обойтись. И это будут не только системные батники-cmd, но и макросы PLC Control.


Макросы


Другая возможность — написать для среды разработки небольшую библиотеку макросов, добавить ее в проект и запускать экспорт непосредственно из меню PLC Control.

Я буду хранить библиотеку макросов в файле Git.mac. Название выбрано не случайно, но об этом чуть позже. Содержимое .mac файла:

MACRO
MacroName: 'Git Commit'
MacroMenu: '&Commit'
MacroCmd: 'file save'
MacroCmd: 'query off ok'
MacroCmd: 'replace yesall'
MacroCmd: 'project expmul ~project-filename-pro'
MacroCmd: 'system git add *'
MacroCmd: 'system git commit -m "%date%-%time%"'
END_MACRO

Кратко, что здесь происходит:
  1. Сохраняем текущие изменения в файл проекта, делаем все по тихому и без вопросов: query off ok; replace yesall.
  2. Экспортируем с разбивкой модулей по отдельным файлам .exp в подкаталог project-filename-pro с помощью project expmul.
  3. Добавляем все изменения на git stage.
  4. Коммитим с автоматическим комментарием в виде текущих даты-времени.
Все просто. Из недостатков — коммиты не содержат внятных комментариев и необходимость вручную вписать вместо project-filename-pro имя файла проекта. Например, есть project-name.pro, в .mac файле будет записано как project expmul ~project-name, то есть просто отбрасываем расширение файла.
Перед экспериментами не забудьте создать репозиторий через git init.

Добавляем макрос в рабочий проект. Идем в Project → Option → Macros и добавляем библиотеку макросов Git.mac через кнопку Macrolibrary → Include...



После этого появится новый раздел меню Edit  Macros → Git  Commit. Несмотря на то, что макрос назван Git Commit, система взяла за основу имя файла библиотеки макросов, то есть просто выдернула его из имени файла Git.mac. Поэтому название файла библиотеки имеет значение.



.Gitignore


Немного приберемся в каталоге проекта. Файл git-игнора может содержать следующие расширения файлов: *.dfr, *.pro, *.bak, *.ci, *.wbp. Эти файлы бинарные, поэтому лучше чтобы их не было в репозитории Git.


Git commit


Я предпочитаю использовать командную строку вместо макросов, поэтому сделал себе .cmd файл который сканирует каталог проекта в поисках .pro файлов, а затем каждый из них экспортирует в свою собственную папку вида ~имя-pro-файла. После экспорта командный файл автоматически коммитит изменения в локальный репозиторий Git. Количество .pro файлов не ограничено.

Командный файл +commit.cmd:

@echo off
SETLOCAL
set TcPlcCtrl="C:\TwinCAT\Plc\TCatPlcCtrl.exe"

if exist "*.pro" (
    FOR %%i IN ("*.pro") DO (
        CALL :ExportProject %%~ni
    )
)

del %cmd%

git add *
git commit -m '%date%-%time%'

exit /b

:ExportProject

set ProjectName=%1

set ProjectPath=%ProjectName%.pro
set cmd=__project_export.tmp
set ExportTo=~%ProjectName%

echo replace yesall > %cmd%
echo query off ok >> %cmd%
echo file open %ProjectPath% >> %cmd%
echo project expmul %ExportTo% >> %cmd%
echo file close >> %cmd%
echo file quit >> %cmd%

%TcPlcCtrl% /show hide /cmd %cmd%

exit /b

ENDLOCAL


Восстановление проекта


Если вы вдруг решите откатить текущие изменения git checkout или перескочить на другую ветку git branch, то сначала необходимо выполнить требуемые команды Git и только затем сделать импорт .exp файлов.

Я опять-таки всё автоматизировал, но только импорт. Еще раз сначала придется выполнить команды Git, и только затем выполнять +restore.cmd:

@echo off
SETLOCAL
set TcPlcCtrl="C:\TwinCAT\Plc\TCatPlcCtrl.exe"

choice /c YN /T 30 /D N /M "Do you want to restore working dir up to the last commit [default = N]"
if %ERRORLEVEL% EQU 2 EXIT 1

if exist "*.pro" (
    FOR %%i IN ("*.pro") DO (
        CALL :RestoreProject %%~ni
    )
)

del %cmd%
exit /b

:RestoreProject

set ProjectName=%1

set cmd=__project_import.tmp
set ProjectPath=%ProjectName%.pro
set ImportFrom=~%ProjectName%

echo replace yesall > %cmd%
echo query off ok >> %cmd%
echo file open %ProjectPath% >> %cmd%
echo project import %ImportFrom%\*.* >> %cmd%
echo file save
echo file close >> %cmd%
echo file quit >> %cmd%

%TcPlcCtrl% /show hide /cmd %cmd%

exit /b

ENDLOCAL

October 10, 2019

Программная активация удаленного доступа

В новых ПЛК по умолчанию отключен удаленный доступ. Во имя безопасности, конечно же. И вот когда ты подключаешься удаленно через эр-ди-пи, ви-пи-эн и прочие тим-виверы, запускаешь CERHost, а на контроллере не активен Remote Display... На помощь приходит Beckhoff Device Manager.

Что делать, если не приходит?

Найти контроллер через System Manager, подключиться к ПЛК и залить в него хитрую программу, которая в свою очередь активирует удаленный доступ.


Автостарт через реестр


Реестре содержит раздел Launch90, в который можно записать имена программ для автостарта. Название ключа явно позаимствовано из Linux, но нам нужно активировать удаленный доступ, а за него на стороне ПЛК отвечает сервис CeRDisp.exe. Теперь всё готово, осталось подать материал в жерло FB_RegSetValue из TcUtilities.lib.

После добавления записи в реестр, необходимо перезагрузить ПЛК. Для этого воспользуемся функцией NT_Reboot из той же библиотеки. Результат:

PROGRAM MAIN
VAR
    Reboot: NT_Reboot;
    RegSetValue: FB_RegSetValue;

    newValue: STRING := 'CeRDisp.exe';
END_VAR
VAR CONSTANT
    HKLM_INIT_SUBKEY: STRING := 'init';
    REG_LAUNCH90_VALUE: STRING := 'Launch90';
END_VAR

(* @END_DECLARATION := '0' *)

RegSetValue(
    sNetId   := '',
    sSubKey  := HKLM_INIT_SUBKEY,
    sValName := REG_LAUNCH90_VALUE,
    eValType := REG_SZ,
    cbData   := LEN(newValue) + 1,
    pData    := ADR(newValue),
    bExecute := TRUE
);

IF NOT RegSetValue.bBUSY THEN
    Reboot( NETID := '', START := TRUE);
END_IF

END_PROGRAM


Что-то еще про автостарт приложений можно прочитать в Automatic start of CE applications. И автозапустить что-нибудь еще полезное.

October 9, 2019

TF1810. TwinCAT 3 PLC HMI Web

У Бекхоффа есть несколько видов визуализаций в которых иногда можно запутаться. Сейчас меня интересует веб-визуализация TF1810 устанавливаемая на ПЛК. Работает и запускается этот вид визуализаций непосредственно на ПЛК. Просмотр и управление тех. процессом производится оператором через браузер с удаленного компьютера.

Работает всё это через JavaScript и HTML5. На контроллере обычно уже установлен, настроен и работает IIS (Microsoft Internet Information Services). При подключении, клиенту отправляются данные только отображаемых на экране элементов. Данные отправляются (т. е. обновляются) циклически независимо от того изменились они или нет. Сам микро-веб сайт разворачивается на ПЛК в каталоге /TwinCAT/3.1/Boot/Plc/Port_851/Visu. Номер порта (Port_851) может быть другим, соответственно путь также немного изменится.

Клиент подключается и рабоатет через браузер: http://plc-name-or-ip-address/Tc3PlcHmiWeb/Port_851/Visu/webvisu.htm. Веб страница в браузере содержат два канваса (Canvas) на которых рисуется вся графика визуализации. Один канвас рисует фон (на случай статичного фона), другой канвас рисует активные элементы. Для отображения графики элементов используется векторный SVG.

В результате всё это работает достаточно быстро, даже при наличии нескольких клиентов. ПЛК нагружается только в рамках прокачки данных по сети через ADS. Графика и прочие визуальные контролы рисуется и нагружают только браузер клиента, никак не влияя на производительность ПЛК.


Visu Profile в 3.1.4024


Начиная с версии 3.1.4024 что-то кардинально поменялось в кухне проектов TwinCAT и теперь необходимо согласовывать версии визуализаций.
  1. Сначала запустить XAE без проекта.
  2. Выбрать необходимую версию TwinCAT.
  3. Загрузить проект над которым вы работаете.
  4. Выбрать в проекте Visu Profile "постарее".

Одновременно работать с разными версиями TwinCAT можно с помощью TwinCAT Remote Manager.

July 4, 2019

Activating System NC PTP Module

There is a special operation in TwinCAT 2 called 'level-up'. That is for PLCs based on Windows CE or Windows Compact only. You take plain PLC, level-up it and get PLC with Numerical motion Control Point-To-Point.

You have to own license key for this special level-up utility. This job was made special for service engineers but you can do it by yourself. Anyway you will get this:


Sometimes, after level-upping NC branch may absent in System Manager configuration. I.e. PLC already can do but System Manager does not allow to configure it.

Actually, we have rised the reg level but forgot to load TwinCAT system module.

TwinCAT 2 is a module (or composite) system. It consist of several low level drivers for different subsystems: TcIo.sys, TcRouter.sys, TcRTime.sys, TcNc.sys... In the case of WinCE, drivers are already integrated into the PLC image. You just have to plug-in this drivers.

The list of loading modules is located at \Hard Disk\TwinCAT\DefaultConfig.xml. Open it and insert new xml-section with TCNC module. See next:

Then we restart the PLC and get the result. It became much better:


Загрузка системного модуля NC PTP

В ПЛК на базе WinCE / Compact существует операция подъема уровня лицензии TwinCAT 2: был стандартный уровень PLC, а стал контроллер с NC PTP и возможностью управления движением.

Для подъема уровня TwinCAT используется специальная утилита, которая, конечно же, требует лицензионный ключ. Вообще, такой процедурой занимается сервисная служба, но можно и самому, а итогом послужит следующая картина:


Правда итог не так очевиден из System Manager: ветка NC может отсутствовать в конфигурации. Иными словами, контроллер уже может, а вот System Manager почему-то в упор не видит NC.

На самом деле уровень лицензии подняли, а модуль в TwinCAT еще не загрузили.

TwinCAT 2 — это модульная система. Она состоит из нескольких низкоуровневых драйверов для подсистем: TcIo.sys, TcRouter.sys, TcRTime.sys, TcNc.sys... В случае с WinCE драйверы уже встроены в образ, их просто нужно подключить.

Список модулей TwinCAT 2, загружаемых при старте системы, хранится в \Hard Disk\TwinCAT\DefaultConfig.xml. Открываем этот файл и дописываем загрузку модуля TCNC. Соответственно файл после редактирования будет выглядеть так:

<?xml version="1.0" encoding="UTF-8"?>
<TcBootProject>
    <Drivers>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCIO</ServiceName>
        </Driver>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCPLC</ServiceName>
        </Driver>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCRTIME</ServiceName>
        </Driver>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCNC</ServiceName>
        </Driver>
    </Drivers>
    <InitCmds/>
</TcBootProject>


Перезапускаем ПЛК и смотрим на результат. Было → стало:


July 3, 2019

Дребезг на трех языках

Паттерны и шаблоны можно находить в любой системе и на любом языке программирования. У Скотта Витлока (Scott Whitlock) в блоге про автоматизацию есть любопытная подборка паттернов (или шаблонов) Patterns of Ladder Logic Programming на языке лестничных диаграмм, релейной логики или просто LD. Всё как в большом и кровавом энтерпрайзе с рейтингом R.


Обычно в проектах я использую ST и C# за редким исключением, когда возникает необходимость в других языках, поэтому было особенно интересно посмотреть как будет выглядеть один и тот же алгоритм на трех разных языках программирования МЭК. Я взял паттерн "Дребезг контактов" (Debounce) и переписал его на LD, ST и CFC.

FBD брать не стал, так как есть CFC, а это более продвинутая и современная версия блочных диаграмм. На картинке выше, адаптация паттерна на языке LD под TwinCAT 3. Про объявление переменных чуть позже.

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


Continuous Flow Chart


Паттерн настолько простой, что перенос не должен вызывать затруднения, разве что расставить последовательность операций, ну и бывает трудно найти панель инструментов с кнопками элементов диаграммы. Она прячется во View → Toolbox (Ctrl + Alt + X). Она же хранит элементы для LD и прочих конструкторов.

Объявление переменных остается прежним. Забегая вперед, оно будет таким же и для LD.


Structured Text


LD легко транслируется на ST. Интересно, что шапка с объявлением переменных и ФБ остается по прежнему одинаковой для всех трех языков:

FUNCTION_BLOCK DebounceLD // DebounceST // DebounceCFC
VAR_INPUT
    IN: BOOL;
    Delay: TIME := T#50MS;
END_VAR
VAR_OUTPUT
    Q: BOOL;
END_VAR
VAR
    DelayOn: TON;
    DelayOff: TOF;
END_VAR

Копируем в программу на ST шапку с объявлением переменных, переименовываем название функционального блока в DebounceST и дописываем тело программы. Текст лаконичнее картинки, редкий случай когда лучше один раз прочитать, чем долго скользить взглядом по картинке. Хотя, на вкус и цвет...

DelayOn(IN := IN, PT := Delay);
DelayOff(IN := IN, PT := Delay);
Q := DelayOn.Q OR (Q AND DelayOff.Q);

Количество строк (равное трем) также совпадает с тремя "ступенями" лестничной диаграммы LD.


Испытания


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

PROGRAM MAIN
VAR
    DebLD : DebounceLD;
    DebST : DebounceST;
    DebCFC: DebounceCFC;

[...]

lastExecTime := _TaskInfo[1].LastExecTime;

Timer(IN := TRUE);
IF Timer.Q THEN
    Timer(IN := FALSE);
    state := SEL(state >= 3, state + 1, 0);
END_IF

FOR i := 0 TO 10000 DO
    CASE state OF
    1:
        DebLD ( IN := in, Delay := delay, Q => outLD  );
        
    2:
        DebCFC( IN := in, Delay := delay, Q => outCFC );

    3:
        DebST ( IN := in, Delay := delay, Q => outST  );
    END_CASE
END_FOR

На самом деле правильность работы я определял по цифровому осциллографу, там же случайно выяснилось, что один из языков незначительно проигрывает в производительности... Это все очень не точно и не имеет большого значения, но все-равно интересно:


Синяя линия отражает значение переменной state, нулевое значение которой соответствует пустому циклу (когда внутри цикла практически ничего не происходит). Заметен небольшой всплеск активности в момент работы CFC функционального блока.

Правда все это крутилось внутри ноутбука, а я сталкивался с ситуацией, когда рантайм настоящего контроллера отличался от рантайма на ноуте. Поэтому я специально перепроверил поведение на 32-х разрядном контроллере CX9020 под WinCE.

Ситуация аналогичная:

Только не воспринимайте эти результаты всерьез: языки все равно равноценны и равнозначны. Пишите на том, который вам нравится или удобен в каждом конкретном случае.