пятница, 9 января 2026 г.

Класс QString для C++Builder

ссылка для скачивания

 

Реализация Qt-подобного строкового класса для C++Builder

Если совсем кратко говорить, то суть в том, чтобы использовать Borland'овский стандартный класс UnicodeString через методы QString из Qt. Т.е. речь идет не о чем-то новом в плане разработки, а лишь об унификации стандартов: если уже есть опыт с QString и нет желания вникать в особенности UnicodeString, то можно использовать этот класс. Также использование классов с одинаковыми методами может быть удобно с точки зрения переносимости кода между разными средами разработки. Упрощенные реализации QChar и QStringList сделаны лишь в том контексте, что они требуются классу QString, не более того.

    QChar — упрощённый аналог Qt::QChar:

  •         Хранит один символ (wchar_t).
  •         Методы для проверки типа символа (isDigit, isLetter и т.д.).
  •         Преобразование регистра (toUpper, toLower).


    QString — основная строка:

  •         Внутреннее хранение через UnicodeString
  •         Поддерживает создание из разных источников: char*, wchar_t*, std::string, std::wstring.
  •         Реализует большинство методов Qt-строки:
  •             Поиск, разделение, замена, регистр.
  •             Подстроки (mid, left, right).
  •             Числовые преобразования (toInt, toDouble).
  •             Кодировки (UTF-8, Latin1, ISO-8859-1).
  •             Форматирование (arg, number). arg() реализована лишь частично (!)
  •             Операторы сравнения и конкатенации.


    QStringList — список строк:

  •         Наследуется от std::vector<QString>.
  •         Методы: join, contains, filter.
  •         Перегруженный operator<< для добавления строк. 

 

  1. Использует встроенные возможности C++Builder (UnicodeString, UTF8String, AnsiString).

  2. Поддерживает набор кодировок и преобразований между ними.

  3. Реализует специфичные методы Qt, такие как section, simplified, toHtmlEscaped.

  4. Учитывает особенности индексации UnicodeString (начинается с 1).

 

Модуль позволяет использовать Qt-подобный строковый API в проектах на C++Builder без подключения Qt, что полезно для миграции кода или поддержки кроссплатформенных исходников с Qt-стилем работы со строками.

понедельник, 7 июля 2025 г.

Динамический массив C++ без реаллокаций

ссылка для скачивания

В архиве три файла для C++ (требуется поддержка компилятором C++11): 


1. Qt

2. Visual C++

3. C++Builder

Версия для Visual C++ является базовой. В версии для Qt добавлена поддержка строковых литералов для QString и работа с QList. В версии для C++Builder добавлена поддержка строковых литералов для UnicodeString.  

 

Реализация динамического массива на C++, обеспечивающего стабильность указателей.

Под стабильностью указателей понимается неизменность адресов в памяти, где располагаются объекты, являющиеся элементами массива. Т.е. указатель-переменная, созданный отдельно от массива и указывающий на любой его элемент, сохранит актуальность независимо от того, какие действия производятся с этим массивом.

Немного об идее.

Идея родилась по ходу разработки конкретного кода, где использовались указатели на элементы динамического массива std::vector и, соответственно, потребовалось сохранять валидность этих указателей при изменении размера вектора. А vector так не умеет. Стал смотреть альтернативы - std::list стабилен, но не имеет индексации - поэтому сразу в топку, std::deque работает через чанки, т.е. разрозненные последовательности элементов, но стабильности в нем нет, там тоже бывают реаллокации. Ну. и в C++Builder, в котором проект я разрабатывал, своих нормальных вариантов тоже нет, TList просто недоразумением показался (C++Builder 6 еще был со старым TList, хотя и новый не лучше). Так вот зародилась идея сделать динамический массив стабильных элементов. Ну, а потом еще выяснилось, что в Qt, на котором я писал программы раньше, начиная с 6 версии фактически ликвидирован класс QList, который превратился в мурзилку для QVector. Суть такая, что в Qt 5 для всех объектов, кроме простых типов, QList как раз и работал  без реаллокации элементов, котрые хранились просто в "куче", а для для всеми любимых realloc существал QVector. Потом кто-то из разработчиков Qt изучил тысячи строк кода и внезапно обнаружил, что многие используют QList там, где следовало бы применить QVector, и не нашел ничего лучше как просто превратить QList в QVector. Так что для Qt 6, если вдруг потребуется динамический массив без реаллокаций, то тоже можно применять класс nx_array.

Не имея желания изобретать велосипед, я взял за основу стандартные std::list и std::vector. Вся идея очень проста: у класса nx_array два поля данных: data_list[] типа std::list и ptr_vector[] типа std::vector. Элементы массива статично лежат в data_list[], т.е. в "куче", без реаллокаций, а ptr_vector[] хранит указатели на эти элементы. Через эти указатели и реализован доступ по индексу.  

Немного о реаллокациях и стабильности указателей.

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

nx_array<QString> my_array;                
my_array += "B";                           // добавили элемент my_array[0] со значением "B"
QString* item_ptr = &(my_array[0]);        // item_ptr хранит адрес my_array[0]
// добавляем в массив новый my_array[0]:  
my_array.addItem("A", 0);                  // item_ptr хранит прежний адрес, который теперь указывает на my_array[1]
 

Методы класса.

Методы nx_array условно разделяются на три категории: 

1. Собственные методы класса, необходимые для комфортной работы. Эти методы отличаются своими названиями, записываемыми без символа подчеркивания, где каждое следующее слово пишется с заглавной буквы, например, метод добавления элемента - addItem(). 

2. Реализация некоторых стандартных методов STL, дающая возможность при желании быстро заменить в готовой программе объекты std::vector на nx_array, не исправляя вызовы методов класса. Речь идет о всем знакомым методах push_back(), pop_back() и т.д. 

3. Методы, являющиеся общими для nx_array и STL, они, всё по той же логики совместимости с std::vector, имеют названия из STL: например, функция, возвращающая размер массива, т.е. количество его элементов, называется size(), а функция возвращающая ссылку на элемент массива с проверкой индекса - at(index).

Конструкторы класса

1. Конструктор без параметров и конструктор с параметрами размера и зарезервированного пространства (резервирование относится только к внутреннему массиву ptr_vector, а не к основному хранилищу объектов):

nx_array<int> arr1;               // пустой массив
nx_array<int> arr2(5);            // массив с 5 элементами
nx_array<int> arr3(3, 10);        // 3 элемента, резерв = 10 

2. Конструктор из std::initializer_list:

nx_array<int> arr1{1, 2, 3};                            // массив с тремя элементами
nx_array<int> arr2({1, 2, 3}, 10);                      // резерв = 10
nx_array<QString> arr1{"a", "b", "c"};                  // массив с тремя элементами
nx_array<QString> arr2({"a", "b", "c"}, 10);            // резерв = 10
nx_array<QString> arr3{{"a", "b", "c"}, 10};            // тоже самое
 

3. Конструктор из rvalue-контейнера

nx_array<int> arr1(std::vector<int>{1, 2, 3}); // Перемещение из временного вектора nx_array<int> arr2(std::list<int>{4, 5, 6}); // Перемещение из временного списка
 

В следующих конструкторах последний bool-параметр означает выбор между копированием (=false) и перемещением (=true):

4. Конструктор из контейнеров:

std::list<int> lst = {1, 2, 3};
nx_array<int> arr1(lst, 10);                        // Копирование из std::list, резерв = 10
nx_array<int> arr2(lst, 10, true);                  // Перемещение, резерв = 10

5. Конструктор из итераторов:

std::vector<int> vec = {1, 2, 3};
nx_array<int> arr1(vec.begin(), vec.end());           // копирование из вектора
nx_array<int> arr2(vec.begin(), vec.end(), 10, true); // перемещение, резерв = 10

6. Конструктор копирования и перемещения:

nx_array<int> original{1, 2, 3};
nx_array<int> copy(original);                       // Копирование
nx_array<int> moved(std::move(original));           // Перемещение
nx_array<int> moved2(original, 0, true);            // Явное перемещение

7. Конструкторы из конкретных контейнеров:

std::array<int, 3> std_arr = {1, 2, 3};
nx_array<int> arr_a(std_arr, 5);                    // Копирование, резерв = 5
std::vector<int> vec = {1, 2, 3};
nx_array<int> arr_v(vec, 10, true);                 // Перемещение, резерв = 10 

8. Конструктор из простого массива:

int raw_arr[] = {1, 2, 3};
nx_array<int> arr1(3, raw_arr);                     // Копирование
nx_array<int> arr2(3, raw_arr, 10, true);           // Перемещение, резерв = 10

// Пример с std::vector: std::vector<int> vec = {4, 5, 6}; nx_array<int> arr3(3, vec.data()); // Копирование из данных вектора


 

Основные методы класса:

bool isPointerValid(const T* ptr) - ищет переданный в функцию указатель в векторе указателей класса. Несмотря на то, что указатели на элементы массива стабильны и в общем и целом можно без опасений создавать переменные-указатели на эти элементы, но указатель всё же потеряет актуальность в случае удаления элемента из массива. По этой причине реализована эта функция поиска элемента по его адресу.

int getItemIndex(const T* ptr) - по сути та же функция, но возвращает не признак того, найден элемент или нет, а его индекс. Если элемент отсутствует, вернет значение -1.

T* getItemPtr(size_type index) - возвращает адрес элемента массива по его индексу.

T& at(size_type index) - возвращает ссылку на элемент массива по его индексу (т.е. тоже самое, что оператор [index]) с проверкой корректности указанного индекса.

size_type size() - возвращает количество элементов массива 

void checkIterator(const const_iterator& it) - функция проверки актуальности итератора

void moveItem(size_type from, size_type to) - перемещает элемент массива на другую позицию

void swapItems(size_type i, size_type j) - меняет два элемента массива местами

void removeItem(size_type index) - удаляет элемент массива по индексу

void removeRange(size_type start, size_type finish = END_POS) - удаляет диапазон элементов массива

void addItem(const T& value, int position = END_POS, bool useExc = false) - добавляет элемент в массив. По умолчанию - в конец массива. Если указан номер элемента, превышающий размер массива, то функция либо добавит элемент в конец (если useExc = false), либо выдаст исключение.

T& emplaceItem(int position = END_POS, Args&&... args) - добавляет элемент с инициализацией

void resize(size_type new_size) - меняет размер массива, т.е. либо удаляет лишние элементы, либо наоборот добавляет пустые элементы в конец массива

void resize(size_type new_size, const T& value) - тоже самое, но со значением по умолчанию для добавляемых элементов

void reverse() - меняет последовательность элементов массива на противоположную

bool sort() - сортирует элементы массива от меньшего к большему. Требует, чтобы типа данных массива поддерживал оператор "<", в противоположном случае функция вернет false

list_type toStdList() - возвращает копию массива в формате std::list

list_type ejectStdList() - возвращает (извлекает) оригинал объекта внутреннего поля std::list класса nx_array через list::splice(), обнуляя исходный массив. Адреса бывших элементов массива сохраняются неизменными, сторонние указатели не теряют актуальности.

list_type toStdList() && - вызывает ejectStdList()

std::vector<T> toStdVector()  - возвращает копию массива в формате std::vector

std::vector<T> toStdVector() && - возвращает std::vector с перемещенными из std::list данными (через std::make_move_iterator), обнуляя исходный массив

std::deque<T> toStdDeque()  - возвращает копию массива в формате std::deque

std::deque<T> toStdDeque() &&  - возвращает std::deque с перемещенными из std::list данными (через std::make_move_iterator), обнуляя исходный массив

QList<T> toQList()  - возвращает копию массива в формате QList

QList<T> toQList() &&  - возвращает QList с перемещенными из std::list данными (через std::make_move_iterator), обнуляя исходный массив

void apply(Function func) - применяет функцию ко всем элементам массива 

T extract(size_type index) - извлекает элемент (через std::move) с удалением из контейнера. Не работает с типами данных, которые нельзя перемещать

T extract(const_iterator pos) - метод, аналогичный предыдущему, но работающий через итератор, а не по индексу. Является аналогом метода extract() в реализации std::list стандарта C++17

 void addArray(const Container& container, int position = END_POS) - присоединение другого массива путем копирования элементов. Поддерживаются массивы типа nx_array , vector, list, deque и QList

void addArray(const Container&& container, int position = END_POS) - присоединение другого массива путем перемещения элементов функцией std::move. Функция аналогичная методу mergeArray(), но требует указания std::move в явном виде при вызове. При вызове для типов данных nx_array или std::list вместо std::move будут вызваться функции spliceArray. 

void addArray(const T* arr, size_type size, int position = END_POS, bool useExc = false) - функция, аналогичная предыдущей, но для добавления копии простого массива. Требует указания его размера.  При useExc = true провоцирует исключения при указании нулевого адреса массива или некорректного параметра position

void spliceArray(list_type& lst, int position = END_POS) - присоединение массива std::list через list.splice, т.е. с сохранением присоединяемых элементов в исходных областях памяти. Указатели, созданные для этих элементов до выполнения функции, сохранят актуальность. Присоединяемый массив обнулится

void spliceArray(nx_array& arr, int position = END_POS) - метод, аналогичный предыдущему, но для nx_array . Поскольку nx_array хранит данные в std::list, то для него также доступно перемещение элементов через list.splice

void mergeArray(Container& container, int position = END_POS) - присоединение другого массива путем перемещения элементов функцией std::move. Поддерживаются массивы типа vector, deque и QList. Не используется для list и nx_array , поскольку для них существует более эффективная функция spliceArray()

Операторы, используемые в nx_array.

Как и в случае с функциями добавления элементов, операторы различают копирование и перемещение объектов через rvalue-ссылки (std::move) или функцией list.splice. Для присвоения копий элементов других классов-контейнеров используется оператор "=", а для добавления к существующим элементам таких копий - операторы "+" и "+=". Для присвоения другого контейнера путем перемещения его элементов используется оператор "<<=", для добавления элементов путем перемещения - "<<".

nx_array<QString> arr1 = {"X", "Y"};

nx_array<QString> arr2 = {"Z"};


arr1 += "A" + QString("B") << arr2; // arr1 = {X, Y, A, B, Z}

                                    // arr2 - пуст 

arr1 += ("A" + QString("B")) << arr2;  // более корректная запись (учитывая, что << имеет более высокий приоритет, чем +)

Оператор [index] работает аналогично стандартному массиву, vector'у и т.п. 

Методы, реализованные для удобной замены std::vector на nx_array.

Методы, возвращающие итераторы: begin(), end(), rbegin(), rend(), cbegin(), cend(), crbegin(),  crend() 

UPD
Добавлено публичное поле isDeleteItemsOnDestroy типа bool. Поле используется для массива, хранящего указатели, применительно к которым следует выполнить команды delete при удалении массива (объекта класса nx_array) из памяти. Поле isDeleteItemsOnDestroy немного схоже с свойством OwnObjects класса TList из Delphi и C++Builder. Однако, в отличие от OwnObjects оно управляет исключительно поведением деструктора ~nx_array(), а не каких-либо других методов класса. Если требуется выполнить команды delete применительно к удаляемым элементам динамического массива nx_array, которые являются указателями на объекты классов, то для этого специально реализованы отдельные методы, имеющие постфикс "_with_delete":


clear_with_delete()

iterator erase_with_delete(const_iterator pos)

removeItem_with_delete(size_type index) 

removeRange_with_delete(size_type start, size_type finish = END_POS) 

replace_with_delete(size_type index, T new_item)


четверг, 16 февраля 2023 г.

Проблема установки WinCC для TIA Portal из-за наличия уже установленной версии

 В продолжение предыдущего поста по поводу установки WinCC. Дошло до установки TIA Portal v15 при наличии уже установленной версии v13. Установщик на v15 общий - и для Step 7, и для WinCC, ну, и при старте выдает вот такое: 

"TIA Portal STEP 7 Professional V15.1 - WinCC Professional V15.1 cannot be operated in parallel with the currently installed version of 'WinCC Professional' or 'WinCC Runtime Professional'. Please remove..."

Тут история такая же, как и с SQL Server из предыдущего поста. Siemens опять развел панику на ровном месте, потому что на самом деле разные версии WinCC Flexible прекрасно уживаются в одной операционной системе. Соответственно, нужно опять подчистить конфигурационный файл установщика, чтобы снять эти ограничения. Файл тот же самый, что и в истории про SQLServer:

 \InstData\Resources\SIA2.ini

В нем находим все упоминания TermMessage9, TermMessage13, TermMessage16, TermMessage17, TermMessage31, TermMessage32, TermMessage36 и удаляем их. Далее установка проходит без эксцессов.

Ну, и если вы хотите быстро находить в установках Siemens файлы, где прописаны подобные ограничения (а это могут быть не только SIA2.ini), то рекомендую использовать бесплатную программу Searcher. Например, установщик требует что-то удалить (remove), так давайте по этому слову и найдём все текстовые файлы *.ini, где содержится слово "remove":

Вот, собственно, и всё.




среда, 15 февраля 2023 г.

Проблема установки WinCC для TIA Portal v11 из-за SQL Server 2005 / 2008

Как известно, старые версии TIA Portal актуальности не теряют (из-за отсутствия совместимости версий для внесения изменений и осуществления Upload). И тут возникла проблема с установкой WinCC Professional v11 из-за уже установленного SQL Server 2008. Установщих TIA Portal v11 устанавливает, помимо всего прочего, SQL Server 2005. Но по какой-то неизвестной причине Siemens решил, что обе версии SQL Server - 2005 и 2008 - не могут существовать в одной операционной системе.

 Чтобы отключить это ограничение, нужно отредактировать файл \InstData\Resources\SIA2.ini. В разделе TERMS нужно закомментировать (поставить ";" в начале строки) или удалить эти две строки:

{Registry};{HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\100\Tools\Setup\Version};{REGSZ};{NOTEXIST};{0};{TermMessage30};{0} 

{Registry};{HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\100\Tools\ClientSetup\CurrentVersion\CurrentVersion};{REGSZ};{NOTEXIST};{0};{TermMessage30};{0}

среда, 4 мая 2022 г.

Реализация RS-триггера (Latching Relay) на Siemens LOGO! для сети Ethernet

Сегодняшняя история про Siemens LOGO!, а, как всем известно, любая история про Siemens LOGO! - это всегда небольшой танец с бубном. Вот и в этот раз понадобилось, казалось бы, совершить простейшее действие, а оно оказалось не таким уж и простым...

Общие сведения

 
Но начнём с общей информации. Как вообще работает коммуникация LOGO! с панелями оператора? Смотрите, есть такие способы коннектиться с LOGO!:

1. Через драйвер, который в панелях Siemens так и называется "LOGO!". Этот драйвер есть на новых панелях оператора, присутствующих в WinCC в составе TIA Portal'a: KTP, TP и др. Есть этот драйвер и на WinCC Flexible Smart. Но этого драйвера нет ни в новых WinCC Runtime, ни WinCC Flexible Runtime.

2. Через драйвер, который называется "SIMATIC S7 200". Внезапно драйвер для контроллера S7-200 подошёл к LOGO. Причём, он не просто подошёл, а полностью идентичен по части работы. Так что если для панели оператора не доступен драйвер "LOGO!", но есть драйвер S7-200, то можно использовать его. На той же WinCC Flexible Runtime, например, такой способ является самым удобным. А вот на WinCC Runtime из TIA Portal'a драйвера S7-200 почему-то нет...
Сразу, кстати, проясняю насчет драйвера для S7-300/S7-400: через него работать не будет.

3. Через OPC Server. Лично я использую Multi-Protocol MasterOPC Server от компании Insat. Проблем пока не было. Но этот способ, понятное дело, только для PC.

4. Через Modbus TCP. Тут уже, ясное дело, не только панели оператора, а любые другие устройства можно к LOGO цеплять по Ethernet.
 

Драйверы LOGO! и S7-200

 
Итак, по первым двум способам - через драйверы "LOGO!" и "SIMATIC S7 200". Повторяю, эти способы абсолютно идентичны. Тут всё, в принципе, элементарно: I1, I2, I3, I4, I5, I6, I7, I8, I9, I10... превращаются в тэги с адресами I0.0, I0.1, I0.2, I0.3, I0.4, I0.5, I0.6, I0.7, I1.0, I1.1... соответственно. Тоже самое по битам Q и М. Для всего остального нужно задать таблицу Variable Memory Configuration: Tools -> Parameter VM Mapping... Например, вот так:
 
 
 Соответственно, так мы получили адреса VW0, VW2, VW4, VD6. Далее заводим эти теги в таблицу тегов WinCC / WinCC Flexible, получаем что-то типа того:


Обратите внимание, что у Timer_Period значения должны включать в себя 2 числа. В данном случае числа 1 и 14, например:

Но числовое значение у нас только одно, и в нём 1 и 14 превратятся в 114. Почему не сделано два отдельных числа - это загадка для меня. Назовём это "неудобство номер раз".

 

Драйвер Modicon MODBUS TCP/IP

 

Что касается Modbus TCP/IP, то чтобы к нему подключиться, сначала надо создать соединение в LOGO! SoftComfort:

Tools -> Ethernet Connections...

 


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

Возвращаясь к Modbus. Наши адреса VW0, VW2, VW4... становятся регистрами #1, #2, #3 соответственно. Или в более понятном представлении: 4x400001, 4x400002, 4x400003. Обратите внимание, не 4x4000x, а 4x40000x.

Теперь переходим к "проблеме номер два". Проблема связана со счётчиком. У него число представлено в формате двойного слова. С ним нет проблем при использовании драйверов LOGO! и S7-200, а с драйвером Modbus TCP/IP возникает типичная проблема для Siemens'a, у которого имеется неувязочка по части последовательности передачи младшего и старшего слова данных. А функционала, чтобы поменять их местами, конечно же, нет. Эта тема уже затрагивалась при обсуждении Modbus RTU на S7-1200, и никаких вариантов Siemens не предложил. Какой выход? Если счётчик предназначен для небольших чисел, то можно использовать не весь VD6, а только VW8, что соответствует регистру 4x400005. Как забрать полностью VD6, я не знаю. Может быть, как-то через скрипты в WinCC это можно сделать...

Что касается остальных областей памяти. С ними всё просто: есть зарезервированные адреса.


Т.е. заводим в таблицу тегов, например, 0x8193 - это будет у нас Q1.


Как управлять битами


Как все мы знаем, LOGO! не даёт возможностей использовать в логических цепочках биты Q и M, которым до этого не были присвоены значения через катушки Relay Coil. Например:

Здесь, чтобы использовать I3 не нужно ничего: просто добавили контакт, выбрали I3 - и всё хорошо. Чтобы также добавить контакт M1, сначала нужно сделать присвоение, как на рисунке. Без него создать контакт M1 у нас не получится. Следовательно, раз мы сделали присвоение, то пытаться дистанционно управлять таким битом бесполезно. Ситуация, когда управление битом без его использования в программе LOGO! может пригодится касается только дискретных выходов Q контроллера. Т.е. для тех Q, для которых в программе не созданы катушки присвоения, управление можно производить напрямую с панели оператора. 

Чтобы решить проблему с обычными битами, Siemens ввёл дополнительные компоненты:

Выбирая Network input, вы создаете объект с адресом в области памяти V, по умолчанию - V0.0. И уже им можете свободно управлять с панели оператора. Если используете протокол Modbus, то надо не забывать последовательность битов. Например, биты в регистре 4x400006 (VW10) будут располагаться так:


Соответственно, V10.0 - это 4x400006.8 и т.д. В программе LOGO! можем сделать что-то наподобие такого:

Реализация RS-триггера (Latching Relay)

Вот вроде бы как всё хорошо и понятно. Для каких-то компонентов можно расшарить данные через таблицу Variable Memory Configuration, для других задач есть специальные Network-компоненты, где соответствие выставляется в настройках самих этих компонентов. А теперь возникает как бы логичный вопрос: как сделать так, чтобы битом можно было управлять одновременно и с панели оператора, и в программе контроллера? Если помните, то в упомянутом уже S7-200 были три варианта тех самых катушек для присваивания значений битам:

1. Присваивание (=)

2. Установка бита (S)

3. Сброс бита (R)

Вот так это выглядит в инструкции к S7-200:

Однако, в LOGO! нет компонентов для установки и сброса битов. Причина весьма проста: LOGO! SoftComfort не допускает дублирование катушек (и блоков - для случая с FBD). Условно считается, что вся программа на LOGO! выполняется как реальная электрическая схема, а следовательно, многократных присвоений битов быть не может. И хотя программисты прекрасно понимают, что в конечном итоге компилятор выдаст из всех этих блоков самый обыкновенный машинный код, который будет выполняться последовательно, Siemens пока что не делает на LOGO! того, что четверть века назад почему-то спокойно сделал на S7-200. Выход из данной ситуации известен: использование RS-триггеров (Latching Relay). Способ громоздкий и неудобный (поскольку проверки условий установки и сброса бита могут находится в совершенно разных частях программы), но обходимся тем, что есть. И всё было бы хорошо, если бы мы могли задать биту RS-триггера какое-то соответствие в V-memory. Но, к сожалению, возможности это сделать нам не предоставили.

Итак, какие же у нас варианты? Прежде всего надо придумать простой пример. 

Допустим, есть бит который определяет режим работы (0 - ручной, 1 - автоматический режим). Бит этот устанавливается с панели оператора (HMI). И допустим, есть дискретный вход I0, который принудительно переводит этот бит в 0, т.е. в ручной режим. На языке программирования STL это выглядело бы так (второй аргумент в командах R и S я опускаю, чтоб не мешался):

LD  I0.0
R   V9.0      // это вот наш бит, допустим, который на HMI выглядит
              // в виде переключателя "РУЧН/АВТО"

Т.е. мы бы управляли битом V9.0 дистанционно (в программе этот момент бы никак не фигурировал), а дополнительно сбрасывали бы его по I0.0. Но мы не можем так сделать, т.к. у нас нет катушки для сброса бита компонента Network Input, которым мы управляем с панели оператора. Установка и сброс мы делаем через RS-триггер. Следовательно, на языке STL это было бы так:

// V10.3 - это бит, привязанный к переключателю "РУЧН/АВТО" на HMI
// V9.0  - это RS-триггер, определяющий режим работы
LD  I0.0
R   V10.3     // наш бит надо сбросить при блокирующем сигнале I0.0
ON  V10.3     // если I0.0 = true или V10.3 = false => тогда...
R   V9.0      // "РУЧН"
LDN I0.0
A   V10.3     // если I0.0 = false и V10.3 = true => тогда...
S   V9.0      // "АВТО"

Попробуем что-то подобное изобразить для LOGO!:


И вроде бы всё неплохо начиналось.. Но есть один нюанс: как мы будем сбрасывать бит V10.3 по условию, что замкнулся вход I1? Эта та самая вторая строчка "R V10.3" в моем примере на STL. Если мы этого не сделаем, то I1 хоть и обнулит SF006, но NI4 (V10.3) продолжит оставаться равным 1. Значит, на экране режим работы всё также будет отображаться как "АВТО", хотя по факту будет уже "РУЧН".

Как выйти из этой ситуации? Я предлагаю следующее решение: использовать счётчик. Компонент счётчик (Up/Down Counter) это ведь тоже по сути своей триггер. Только у него входы не R и S, а R и Cnt. Cnt прибавляет +1 к текущему значению счётчика. Например, было 0, стало 1. Мы можем задавать значение, выше которого бит счётчика устанавливается, а ниже - сбрасывается. Т.е. зададим это значение равным 1, и бит счетчика будет сбрасываться при 0 и устанавливаться при больше или равно 1. В чём важное для нашей задачи отличие счётчика от RS-триггера? Как было показано выше, мы можем расшаривать текущее значение счётчика и редактировать его на панели оператора. Если же наш счётчик будет работать в пределах 0..1, т.е. иметь всего два возможных значения, то в этом случае получится, что из всего того двойного слова данных, где это значение хранится, нас будет интересовать один-единственный бит. Самый первый бит. Тот, который отличает текущее значение 0 от значения 1. И вся наша задача легко и просто ушла в одну строку:

Следует не забывать, что у компонента "Счётчик" первый верхний вход - это сброс. Важно не перепутать с компонентом "RS-Триггер", у которого сбросом является второй вход.


Как изображено на самом первом рисунке в этой статье, текущее значение счетчика у нас имеет адрес VD6. Соответственно, интересующий нас бит имеет адрес V9.0 (или 4x400005.16 в случае с Modbus). Меняя этот бит, мы меняем значение счётчика. А сам счетчик настроен на включение при значении больше или равно 1:

Вот мы и получили, по сути дела, то, что и хотели получить в самом начале:

LD  I0.0
R   V9.0

Естественно, что мы можем не только сбрасывать бит триггера программно, но  устанавливать его:


 

В данном примере мы управляем дискретным выходом Q1, привязанным к нашему управляющему биту V9.0 блока C003. Помимо того, что мы можем менять значение V9.0 на панели оператора или в СКАДА, также в коде программы входом I1 этот бит принудительно сбрасывается, а входом I2 принудительно устанавливается. Единственный нюанс: счетчик не должен считать больше 1. Поэтому после I2 стоит бит проверки (M10 == false), что счетчик в данный момент = 0. Эту проверку нельзя выполнить напрямую из-за ограничений среды разработки (контакт C003 не может приходить в блок C003), поэтому задействован промежуточный бит M10, что, в общем-то, не сильно загромоздило нам программу.

суббота, 24 апреля 2021 г.

Синий экран смерти при подключении к S7-300 - S7otranx32.sys

Тут на днях вновь всплыла ошибка, возникавшая у меня несколько лет назад при подключении к контроллеру S7-300 через оригинальный Siemens'овский USB/MPI кабель. Выскакивает вот такая "радость":


Почему это происходит и чем лечится я не знаю, прошлый раз это прошло как-то само собой. В любой случае, если такое возникает в "полевых" условиях, есть один 100%-ый вариант как это прекратить. Для этого необходимо иметь под рукой китайский кабель PC-MPI с aliexpress, который подключается к COM-порту (т.е. понадобится еще переходник USB-RS232). COM-порт надо выбрать в настройках Set PG/PC Interface:


При работе с таким адаптером ошибки винды из-за файла S7otranx.sys не будет точно.

P.S.

Раньше на aliexpress был комбинированный адаптер 2в1: PC/MPI + USB/RS232, т.е. он включался в USB и определялся как обычный преобразователь в RS232, работая через драйвер Profilic. Выглядел он вот так:

 

Но, к сожалению, больше таких адаптеров китайцы не делают, все их USB-адаптеры теперь работают только через Siemens'овские драйвера, а не через эмуляцию COM-порта, а конкретно они теперь все используют старую версию Siemens'овского USB-драйвера (версия 2.0, которая берет питание с порта USB, в то время как более новый сименовский драйвер 8.x берет питание с порта MPI), поможет ли использование такого кабеля под старые дрова - я точно сказать не могу. А если требуется через COM-порт подключится (100% вариант, причем, еще и никак не привязанный к версии Windows - что актуально на фоне того, как Microsoft стал агрессивно навязывать новые версии винды), тогда на aliexpress надо покупать именно PC-MPI адаптер (RS232/MPI).

четверг, 11 марта 2021 г.

Тензо-М ТВ-003/05Д: весоизмерительная поделка

Сегодняшняя тема будет про преобразование сигналов тензодатчиков с помощью приборов "ТВ-003/05Д" от компании "Тензо-М". Хочу сразу обратить внимание, что речь идёт не в целом о компании - производителе тензодатчиков, а о конкретном устройстве, потому что если говорить о датчиках, то, в принципе, у меня с ними никогда проблем не было, работают на разных объектах без нареканий. Тут речь о том, что не надо делать то, чего не умеешь: если ты делаешь хорошо тензодатчики, ну так и делай их, и не надо начинать производство каких-то глючных приборов для их подключения. 

Так вот перейдем теперь к ТВ-003/05Д. Это поистине весоизмерительное чудовище - огромное, тяжелое, глючное. В общем-то, чтобы понять, насколько оно неудобное в использовании, достаточно просто заглянуть в инструкции к этим приборам. 

Хочу обратить внимание на несколько моментов, с которыми пришлось столкнуться мне лично при работе с этим оборудованием.

1. Разные модификации. Если кто-то всерьёз думает, что совсем недешёвый, тяжеленный и неудобный в использовании прибор должен быть хотя бы универсальным - это заблуждение. На каждом таком приборе есть специальная наклейка, где производитель ставит пометки напротив опций, которые включены в конкретную модификацию. Т.е. делать просто универсальные приборы, которые имеют на борту интерфейсные выходы RS485, RS232 и аналоговый выход - это слишком сложная задача для отечественного производителя (который и без того неконкурентоспособен по цене, и, казалось бы, ну почему бы не постараться хоть что-то сделать хорошо? но нет...) , поэтому не нужно удивляться, что именно необходимый вам для вашей конкретной задачи способ считывания информации с вашего конкретного прибора ТВ-003 поддерживаться не будет.

2. Кривой интерфейс RS485. Очень долгое  время ответа прибора при опросе. Нет возможности задавать проверку четности или нечетности, параметры - 8N1, других нет, настраивается только скорость передачи данных и адрес. Мало того, дополняя пункт 1, следует также обратить внимание на то, что на приборах разных прошивок еще и скорости доступны разные (!), например на каких-то одних прошивках можно устанавливать скорость (параметр №13 меню прибора) от 1.2 до 9.6 кбит/с, а, например, на версии прошивки dd-107 есть всего два варианта: 9.6. и 19.2 кбит/с.

3. Свой собственный протокол. Данный прибор не поддерживает стандартные протоколы передачи данных. Вместо этого у них собственный протокол, который можно преобразовать в Modbus RTU только при помощи преобразователя стороннего производителя. Называется ДПИ-МТ-1, преобразователь крутой, настраивается через веб-интерфейс, даже имеет на борту wifi для этой самой настройки и... стоит в полтора раза дороже, чем ТВ-003/05Д.

3. Кривой OPC-сервер. Продолжая историю собственного протокола Тензо-М, следует вспомнить и их собственный OPC-сервер. Работает он ужасно, не имеет никаких настроек тегов и не отображает никаких значений (т.е. нет таблицы тегов и значений как в обычном пользовательском интерфейсе любых OPC-серверов). Вы даже не поймёте, что OPC-сервер подключился к устройству, потому что это никак не отображается. Всё, что вы увидите, - это количество клиентов OPC-сервера в данный момент времени (ну, т.е. самую ненужную информацию по сути). Устройства и теги не имеют иерархии, посмотрите инструкцию, что они пишут:

Данный OPC-сервер имеет плоскую структуру, т.е. теги расположены на одной,
главной ветви. Для того чтобы идентифицировать местоположение тега (items), номер
дозатора, которому принадлежит тег, и номер линии, на которой расположен дозатор,
предусмотрена специальная маркировка для всех тегов (items):
LX_DY_имя тега
где X - Номер линии; Y - номер (порядковый) дозатора.
Так, например имя тега принадлежащего 3 дозатору который расположен на 1
линии будет выглядеть как: L1_D1_имя тега.

(о, Господи!) Т.е. они не в состоянии были сделать даже элементарную древовидную структуру, вместо этого они сделали кучу тэгов, которым по именам надо различать, к какому устройству они относятся! Да, можно понять, что производитель делает тензодатчики, это его основное направление деятельности, но неужели так сложно нанять одно-единственного программиста, который сделает нормальный OPC-сервер?

Лично я не смог нормально пользоваться этим OPC-сервером, потому что после подключения к нему мой OPC-клиент на базе wtclient.dll, отлично работающий с любыми OPC-серверами (и Siemens, и InSat, и Овен) просто загнал загрузку CPU компьютера чуть ли не на максимум.

4. Разные меню прибора и способы перехода в него в зависимости от версии прошивки. 

Ну, т.е., по всей видимости, производитель изначально сделал, как ему показалось, не самое удобное меню, и решил его поменять. А потом опять поменять. А потом ОПЯТЬ! Спрашивается, ребята, вы в своем вообще уме? Т.е. на одном предприятии в одном цеху висят одни и те же приборы и настраиваются они все по-разному. У операторов мозг закипает каждый раз, как надо оттарировать ёмкости или выполнить еще какие-то операции, требующие перехода в меню, а также заменить один прибор другим.

Значит, по печатной инструкции от Тензо-М, которая есть у меня, для перехода в меню следует выполнить такие действия:

Вход в режим программирования
Для входа в режим программирования 
1) нажмите клавишу "Ф", при этом на индикаторе высветится "Func"
2) нажмите клавишу "0", при этом на индикаторе высветится приглашение к вводу пароля "oooooo"
3) последователь нажимая цифровые клавиши, введите шестизначный пароль...
4) после безошибочного ввода последнего (шестого) знака пароля терминал перейдет в редим программирования (при этом на индикаторе высвечивается "SEL")...

На половине моих проборов такое нажатие кнопок не давало нужного эффекта.

Теперь почитаем инструкцию, которая называется "Преобразователь весоизмерительный TВ-003/05Д. Версия программного обеспечения 10.13АХ. Руководство по эксплуатации и калибровке" (инструкция с сайта Тензо-М). Там мы узнаем, что все функции выполняются простым нажатием кнопки "Ф" и кнопки с цифрой, но при этом там не указаны такие сочетания как "Ф" + "0" (из предыдущей инструкции") и "Ф" + 2 (о чем речь пойдет ниже). В сервисное меню по этой инструкции надо заходить, нажимая "Ф" + "ВВОД". Такое сочетание кнопок ни на одном приборе не давало у меня эффекта. Наверно, потому что это для прошивки 10.13АX, ну да...

Идём дальше. Методом "научного тыка" я определил, как же мне настроить остальные приборы, для которых первый способ не сработал. Последовательность действий такая:

1. Жмём кнопку "СБР". Загорается номер прошивки (как при включении прибора): dd-107

2. Ждём, пока первый раз проморгает лампа "КОНТР" (промаргивает она два раза)

3. Нажимаем в этот момент кнопку "Ф"

4. Появляется приглашение ввести пароль "оооооо"

5. Вводим 535160. Появляется надпись "SEL"

6. Нажимаем кнопку "2". Итак, мы в меню.

7. Нажимаем кнопку со стрелкой вниз, чтобы переходить по пунктам меню.

Если же нажать "Ф" слишком поздно, то вместо "оооооо" появляется "Func" (как из печатной инструкции), вот только нажатие после этого любой кнопки (в том числе "0") приводит лишь к тому, что на экране просто отображается текущее значение тензодатчика, т.е. перехода в меню или запроса на ввод пароля не происходит.

 

5. Распиновка 25-контактного штекера. 

В упомянутой выше инструкции (той, которая для прошивки 10.13АХ) мы можем увидеть такую распиновку для интерфейсов RS485 и RS232:

2 -- RXD (rs232)

3 -- TXD (rs232)

5 -- GND (rs232)

7 -- Data+ (rs485)

8 -- Data- (rs485)

9 -- GND (rs485)

А потом оказывается, что это распиновка не DB25, а DB9. Вот как хочешь, так и догадайся! Может быть у них есть модификации с DB9 или типа того, но неужто так сложно написать в инструкции три символа: "D", "B", "9"?? 

Короче, подключать RS485 надо так:

13 -- Data-

25* -- Data+

9 -- GND

* на штекерах, которые установлены на моём предприятии (какие-то необычные штекеры под шлейфы), этот пин пронумерован как 14-ый, а не 25-ый, т.е. нумерация идет "змейкой" от конца первого ряда контактов вниз на второй ряд и потом в обратную сторону (14-ый оказывается под 13-ым), а не так, как это нумеруется стандартно на разъемах DB25 (где 14-ый контакт расположен под 1-ым).

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

 Правильная нумерация DB25 (на всякий случай):


ДПИ-МТ-1

Теперь насчёт преобразователей ДПИ-МТ-1. Делает томская компания "Фактор Кода", выглядит в общем и целом как заводское устройство, нареканий по работе нет. ПРЕДНАЗНАЧЕН ТОЛЬКО ДЛЯ ОДНОГО УСТРОЙСТВА ТЕНЗО-М. Т.е. если Тензо-М несколько - то и ДПИ-МТ-1 нужно тоже несколько. Как я уже сказал, стоит это устройство дороже, чем сам Тензо-М (больше $200).  

Что не понравилось - это то, что, во-первых, нельзя выбрать скорость 1.2 kbit/s для Тензо-М (хотя сам прибор работает на такой скорости - кроме прошивки dd-107, где почему-то оставили только две скорости: 9.6 и 19.2) и, что самое главное, нельзя установить проверку четности/нечетности, задать число стоп-битов и количество бит данных. Т.е. подключая прибор к существующей сети RS485, сразу необходимо переводить эту сеть на 8N1, что, конечно, может быть неудобным: нацепляешь в одну сеть таких приборов вместе с каким-нибудь овеновским ТРМ202 (у которого 8N2, число стоп-бит нельзя поменять) -- и "добрый вечер"...

Настраивается через web-интерфейс (IP: 192.168.4.1), подключается по wifi. Для протокола Тензо-М можно задавать скорость (от 2.4 kbps) и адрес, тоже самое и для протокола Modbus на выходе. Опрос данных производится по регистрам:

#400

0x0190

40401

FLOAT

Масса нетто

#406

0x0196

40407

FLOAT

Масса брутто

#206

0x00CE

40207

BCD

Масса нетто

#208

0x00D0

40209

BCD

Масса брутто

(!) Для типа данных FLOAT необходимо осуществлять перестановку байт старшим словом вперёд (Кстати! Именно такая последовательность установлена для протокола Modicon Modbus панелей оператора Siemens, так что можно эти приборчики цеплять напрямую к любой панели оператора Siemens, где есть RS485, включая Smart-панели)



В общем, вот так. Как мне кажется, чем покупать глючную чудо-поделку от Тензо-М, которая крайне неудобна для эксплуатации и которой требуется дорогущий преобразователь для диспетчеризации для перехода на нормальный протокол, лучше за те же деньги купить, например, вот это на четыре канала тензодатчиков и какой-нибудь дешевенький PLC и панель оператора к нему (я бы купил китайский S7-200 и Smart-панель к нему на Aliexpress, но есть и более дешевые решения), чтобы сделать всю настройку по-человечески и удобной для использования. 

Ну, и еще раз скажу, что к самим тензодатчикам Тензо-М претензий нет. Такое впечатление, что ребята просто попробовали делать то, что не умеют. Я не знаю, как там обстоят дела с последними моделями - возможно, они что-то в них исправили, улучшили, но в любом случае инструкций нормальных нет, OPC-сервера нормального нет, ценник рядом с китайцами - неадекватный. В компании "Фактор Кода", которая делает преобразователи в Modbus, аргументируют использование приборов Тензо-М с той точки зрения, что они со всех сторон сертифицированные. Возможно, что и так. Поскольку все мы знаем, насколько априори бутафорская, никчёмная и бессмысленная (для потребителя) в нашей стране любая сертификация, то наличие сертификата соответствия - это в 99% случаев просто попытка набить цену на некачественный продукт. Хотя, там, где государство требует все эти филькины грамоты, там без сертификатов не обойтись.


P.S. Кому нужна инструкция к прибору на прошивку DD-107 (или DD-10.7, как она обозначена на некоторых устройствах) - можно скачать тут.