Pull to refresh

Как я проект в OpenSCADA сделал

Reading time14 min
Views81K
image

Речь в статье пойдет о той самой OpenSCADA, которая под Linux и с oscada.org.

Зачем:
• потому что SCADA на самом деле достойна внимания и популяризации;
• в некоторых малобюджетных или маленьких проектах просто безальтернативная;
• судя по статьям про АСУТП на хабре, многим читателям АСУТП представляется черной магией, недо-IT или чем-то похожим (ломают несчастный modbus, мучают WinCC, которая и так еле тарахтит… Люди читают и охают: «Как так можно…. дырявое ПО в промышленности», но никого не удивляет ломание Win95 и 6го ослика. Поломали бы LON шифрованный, OPC, OPC_UA…… А WinCC сама расшаривает папку с проектом с именем вида WinCC_Project_xxxxxx при первом открытии + это вы еще не видели как ее плагин к Excel может намертво винду подвесить при неаккуратной вставке ячеек чуть больше, чем он может за раз осилить!) – добавим ликбеза;


Как вам такое?


А такое?


Индусы вообще не любят обрабатывать ошибки… Зачем, и та-а-ак сойдет…

Вообще, единственные, кто работает в Сименсе — это МЕНЕДЖЕРЫ. Им при жизни надо памятник ставить. С такими менеджерами программистам можно не напрягаться.

• хабр приносит пользу мне (в плане информации) – принесу хабру и я;
• избавится от read-only для участия в комментариях.

Для кого:
• для тех, кто не слышал про openSCADA да и вообще про OpenSource SCADA под Linux, но интересуется подобным;
• для тех, кто слышал, но все не было времени попробовать, дабы понять, нужно ли оно;
• для тех, кто открыл – посмотрел — не разобрался — закрыл;

Почему openSCADA:
• в данном проекте требовалось сэкономить;
• ТЗ не заставляло использовать что-либо конкретное;
• в данном случаем можно использовать связь с ПЛК по Modbus (например, если б требовалось передавать алармы с меткой времени ПЛК, то увы….).

Дано:
• парочка генераторов по полтора мегаватта;
• место оператора для управления всем этим;
• ПЛК B&R, связь со SCADA по ModbusTCP.

Что надо:
• мониторинг состояния узлов и агрегатов;
• управление узлами и агрегатами;
• фиксирование аварийных и предупредительных событий (алармы);
• ведение архива событий;
• архивирование значений параметров.

Должен сказать, что ее автор вполне грамотно подошел к задаче (даже получил второе высшее как программист, первое — электронщик). Использует CPP, Qt, svn и т.п. Т.е. самую свежую версию вы можете скачать и собрать, не дожидаясь оформления релиза. Отдельно надо сказать о обратной связи по багам. Заметили багу? Пишите на форуме в разделе «Отслеживание ошибок» и, вуаля, она исправлена. Конкретные примеры: от 40 минут до 4 часов, причем был случай и после 12 ночи. Можете исправить сами и Рома добавит. А теперь скажите, кто из гигантов производства, хотя бы промышленного ПО, делает хотя бы всего лишь на порядок дольше? Например MS для MS Project ……. М?

Вам недостаточно документации в wiki? Заходите на вики под своим форумным ником и дополняете.

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



На первый взгляд, у openSCADA высок порог вхождения да и сам разработчик говорит, что ваш предыдущий опыт работы со SCADA вряд ли пригодится, но это только на первый взгляд. Все есть непонятно, непонятно называется, непонятно, как это работает. Документация не особо-то структурирована. Пример — результат поиска по сайту изобилует фразами «аргумент атрибута параметра»:



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

image
Пример объяснения концепции взаимодействия внутренностей openSCADA

Составляющие openSCADA:
QTCfg – QT конфигуратор (сбор данных, вычисления, архивы);



Vision – графический интерфейс пользователя («картинки»);



QTCfg — БД




Основа этой SCADA – базы данных, т.е. все проекты хранятся в БД. Может это и хорошо, но вот для простого «скопировать проект из папки Х на флешку» или «скопировать клапан с одного проекта в другой» не тривиально. В случае с БД SQLite вся база есть файлик и копировать из папки на флешку не сложно. А для PostgeSQL? С копированием графического элемента из одного проекта в другой задачка. Хотя никто не мешает вести библиотеку чего-либо в отдельной БД и в свой проект просто подключать нужный экземпляр. Все таблицы всех баз можно посмотреть и есть поле ввода SQL запроса, так что можете в табличном редакторе склеить запросы и массово что-то внедрить в свой проект вместо бесконечного тыкания мышкой.

QTCfg — Контроллер




В сборе данных настройка связи с нашим ПЛК B&R проста: как часто опрашивать, modbus протокол (TCP/IP, RTU, ASCII), адрес транспорты, modbus-адрес нашего ПЛК B&R и тп. Стоит особенно отметить настройку «Максимальный параметр блока запроса» – это сколько регистров за раз запрашивать у нашего ПЛК. Некоторые устройства просто не отвечают, если запросить более некоторого количества. Например, этим «страдает» корректор газа ЕК-270, панель управления PCC3300 у двигателей Cummins.

Статус вполне информативен, особенно при проблемах связи – есть и текст, и код ошибки, легко обрабатывать в скриптах.

QTCfg — выходной транспорт MBTCP




Настройка связи с ПЛК – как? Есть транспорты и есть транспортные протоколы. Куда? В транспортах есть Сокеты, в Сокетах Выходной и Входной транспорт. ModbusTCP вроде через сокеты, но в Транспортных протоколах есть Modbus.
Настройка связи с ПЛК находится в Транспорты – Сокеты – Выходной транспорт – ИмяМоегоТранспорта. Для modbusTCP конфигурация выглядит незатейливо. Всплывающая подсказка помогает.

В Статусе видим текущее состояние, особенно актуально при отладке, диагностике.

QTCfg – Modbus – диагностика




Для сбора данных с нашего ПЛК B&R создали контроллер в разделе ModBUS. В Runtime на вкладке «Диагностика» наблюдаем обмен пакетами. Если ПЛК не отвечает – тут же видим причину.

QTCfg — Пользовательский протокол




Пользовательский протокол – это вообще мега-фича openSCADA, открывающая ей двери много куда. В данном модуле вы можете описать свой протокол, который отсутствует в openSCADA. Например, те самые modbus-подобные с 32-битными регистрами и т.п. На картинке приведен пример протокола для теплосчетчика ВКТ-7 от Теплоком. Так что можете сами сваять в ПЛК свой modbus-подобный протокол с 64-битными регистрами, метками времени и шифрованием и в openSCADA реализовать его прием.

Более того, вы можете реализовать свой протокол на С++ как *.so и подключить как модуль в Сбор данных при сборке openSCADA. За пример можно взять модуль SNMP.

Свой механизм алармов как раз и хотим оформить как модуль в Сборе данных, а внутри использовать не массив (который не удобен для сортировки), а SQLite с работой в оперативке вместо файлов.

QTCfg — JavaLikeCalc




Все пользовательские функции реализуются в разделе Сбор данных – JavaLikeCalc – Библиотека – ваша_библиотека. Вызов своих функций весьма коряв: SYS.DAQ.JavaLikeCalc[«lib_MyLib»][«MyFunc»], префикс «lib_» у имени библиотеки «MyLib» обязателен. Вариантов вызова функций два: статично прописываем имя функции в коде или динамично составляем имя в коде.

Отладка собственных функций через аналог printf() – просто пишете в Архив, что вам надо. Никаких «по шагам или breakpoint». Подсветка синтаксиса есть. Автообъявление переменных при первом использовании – будьте осторожны с очепятками – создаст новую переменную и в путь. В каждой функции есть объект this – ссылка на саму функции, например, можно в скрипте обращаться к атрибутам функции в цикле и т.п. Так же есть флаг f_start – флаг первого исполнения функции, туда удобно засунуть инициализацию или что-то подобное одноразовое.

QTCfg – Пример собственной функции prioritets на языке JavaLikeCalc




Пример создания пользовательской функции. Язык – JavaLikeCalc (он же единственный). Входные/выходные переменные функции описываете в таблице IO. Порядок имеет значение: 1 строка = 1 переменная при вызове функции. Менять местами строки в IO можно.

Обращаться к тегам можно несколькими способами. В openSCADA вообще одно и то же можно сделать как минимум двумя способами. Доступ к тегу (атрибуту в терминах openSCADA) в коде можно осуществить так SYS.DAQ[«JavaLikeCalc»][LocalPLC][Local_Param][«myTag»].get(), где " JavaLikeCalc " – статично заданный модуль Сбора данных, "LocalPLC" — имя контроллера в модуле Сбора данных, заданный через переменную, " myTag " – статично заданное имя тега, get() – функция запроса значения тега.

В JavaLikeCalc синтаксис употребления встроенных функций таков: strMyTag.StrToInt()? где StrToInt() – функция конвертации текста в целое число, strMyTag – наша текстовая переменнаяю. Эта особенность синтаксиса употребления встроенных функций привела к тому, что в Vision доступ к свойствам объекта осуществляется не через точку (MyObject.property1), а через нижнее подчеркивание (MyObject_property1), что значительно снижает читабельность, поскольку "_" в именах тегов используется вместо пробела.
Еще пример синтаксис: this[«pgCont»].attrSet(«geomY»,soOff).attrSet(«geomH»,this[«pgCont»].attr(«geomH»)+soSize)

QTCfg – исполнить функцию




Вкладка «Исполнить» позволяет проверить свою функцию не отходя от кассы. Плюс покажет затраченное на выполнение функции время.

QTCfg – вызов стороннего приложения на JavaLikeCalc (на примере моей функции play_warn)




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

QTCfg — SETS




Для «локальных» тегов используем контроллер в разделе JavaLikeCalc. Контроллер назвали «LocVars», в нем создали а-ля группы тегов actions, calibrs, sets(«параметры» в терминах openSCADA). Эти параметры полезны и использованы неслучайно. С ними связан удобный механизм привязки графики к тегам в графическом редакторе Vision. Мы еще к этому вернемся. В поле «Поля данных» пишем имена наших «локальных» тегов. Эти теги сохраняют свои значения после перезагрузки компьютера.

QTCfg — SETS values




Значения наших «локальных» тегов можно посмотреть и задать на вкладке «Атрибуты». В данном случае «sets» – это просто логическая группа тегов, а сами теги в терминах openSCADA – это атрибуты. Какая SCADA позволяет подобное в столь же простой форме?

QTCfg — actions




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

QTCfg — параметр G1




Параметром зовется логическая единица с набором тегов. У нас деление по узлам и агрегатам. «Включен» – обрабатывается ли он в текущий момент, «Включать» – включать ли при запуске SCADA.

Конфигурация тегов и адресов в случае Modbus просто песня – это просто текст, который легко склеивается в табличном редакторе, легко правится в openSCADA. Например, по проекту у вас есть теги read-only, вы их оформили в регистры 30ххх. В openSCADA это выглядит как RI_b15:101:rw:TagName:TagComment. В таком случае на вкладке Атрибуты вы не сможете задавать значения тегов, хотя и прописали rw. Легким движением руки в любом блокноте заменам RI_ на R_ и у вас уже регистры 40ххх. В таком случае в можете задавать значения тегам на вкладке Атрибуты, что есть намного удобнее, чем вычислять бит в слове и лезть в modbus симулятор и задавать в десятичном виде. Если регистр 40ххх, но rw, то задавать значения на вкладке Атрибуты не сможете. RI_b15:101 – это 15 бит в 30101 регистре modbus.

QTCfg — параметр G1 — значения




В Runtime очень просто наблюдать текущие значения тегов. Открываем вкладку «Атрибуты» требуемого параметра контроллера и наблюдаем, если регистр 40ххх и «rw», то и управляем – для отладки незаменимо. Попробуйте повторить подобное в WinCC или Citect …. Хотя в WinCC можно посмотреть значение тега, наведя на него мышку.

QTCfg — параметр G1 — архивация значений




Конфигурация архивирования тега – поставить галку на вкладке «Архивация». Любой тег можно архивировать одновременно в различные архивы.
Что важно: информация в архиве СЖАТА (а не просто миллион строк вида timestamp|tagName|tagValue).

Структура файла:

image

Механизм последовательной упаковки значений:
image

Там еще куча тонкостей типа «жесткой сетки» и «времени высокого разрешения»…

QTCfg – архив сообщений




Конфигурируем архив сообщений: будем хранить в БД, с категорией «LOG» (категория в данном случае просто префикс для фильтрации наших сообщений от системных), размер архива в часах.

QTCfg – архив сообщений – сообщения




Посмотреть свои сообщения в своем архиве – вкладка «Сообщения» у соответствующего нашего архива.

Архив сообщений — папка и файл




Архив сообщений – текстовые файлы (вполне читабельные). Старые файлы закатываеются архиватором gzip согласно настройкам по архивированию сообщений.

QTCfg – архив трендов




Конфигурируем тренды: храним на файловой системе (рекомендация Ромы), как часто считывать значения, как часто в архив записывать, путь для файлов-архивов, размер архива в часах и т.д.

QTCfg – архив трендов – значения




Посмотреть значения архива значений можно тут же на вкладке «Архивы». Есть возможность экспорта в файл *.CSV или *.WAV.

QTCfg – архив трендов – график




Архив трендов на графике не отходя от кассы – не вопрос. Размеры изображения графика можно менять.

QTCfg — конфигурация алармов




Поскольку в openSCADA нет алармов в классическом понимании SCADA систем (вместо них Рома придумал нарушения – некая абстракция не пойми чего, уровень нарушений задается программистом проекта в момент формирования нарушения и не говорит ни о чем), пришлось создать свой механизм. Создали отдельный контроллер на JavaLikeCalc и задали ему мониторить значения нужных тегов на предмет алармов и формировать список активных алармов.

QTCfg — нарушения




Встроенный механизм «проговаривания» нарушений (нарушения у openSCADA – это некая абстракции в сторону алармов). Отсюда взяли команду для проигрывания файлов для собственного механизма озвучивания алармов.

QTCfg – синтез речи




Надо отметить: встроенный механизм «алармов» openSCADA позволяет проговаривать текст сообщения движком tts. Установите tts с русским языком – встроенные «алармы» (т.е. нарушения) заговорят на русском.




Vision-разработка




Переходим к графике. Для этого используется «вторая половина» openSCADA – Vision.

QTCfg – Vision




Настройка Vision из QTCfg: задаем с каким пользователем откроется Vision в разработке и в Runtime графический интерфейс пользователя, какой проект запускать в Runtime.

Vision-разработка — виджеты




Виджет – это графическая единица (клапан, задвижка, генератор, поле ввода и даже целая страница).

Вкладка Виджеты – здесь находятся все виджеты, собранные в библиотеки. Это некая общая база хранения графических единиц.
Есть возможность в один виджет-страницу динамично в Runtime вставлять другие виджеты. Аналог WinCC-шных PictureWindow. Для этого на страницу надо поместить виджет и задать ему свойство «контейнер» и свойству группа дать какое-нибудь имя. Вставляемый виджет должен иметь ту же группу.

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

Vision-разработка – проекты




А вот на вкладке Проект мы уже собираем скелет графической части нашего проекта из экземпляров библиотечных виджетов (набираем страницы и определяем навигацию по ним). В Проекте мы можем задавать персональные свойства библиотечным виджетам – например, виджет «окно с данными от цифрового мультиметра» – создаем 5 окон для пяти высоковольтных ячеек и привязываем к тегам соответствующих ячеек.

Vision-разработка — групповое изменение свойств




Очень удобная опция: групповое изменение свойств нескольких объектов – выделили объекты и сразу всем задали.

Vision-разработка — text – аргументы




У стандартного графического элемента «Text» есть очень полезная особенность – аргументы.

Таким образом, один элемент Text позволяет реализовать законченный форматированный элемент мнемосхемы вида «P = 32, кВт», где «P» – обозначение параметра, «32» – его значение (значение тега), «кВт» – единицы измерения. Т.е. динамически привязывает только значение тега, остальное уже есть. Формат прост: «P = %1, кВт» (вместо %1 будет значение тега).

Vision-разработка — рисование фигур




Вот с рисование не очень. Рисование геометрических фигур возможно только в неком контейнере типа Box. Как видно на рисунке в поле Список элементов линии представлены в текстовом виде line(15.359|0.505):(15.359|22.915):::::::::::. Попробуйте угадать кто где … Но не так все грустно. Как видно на рисунке под меню файл есть кнопки для рисования прямых, кривых Безье, окружностей. А что грустно? Например, окружность задается по пяти точкам, а не привычным прямоугольником/квадратом.



Меняя координаты любой из пяти точек прочее получить черти-что вместо того, что надо. Заливка цветом возможна только для замкнутого контура, т.е. обычный столбик с заливкой цветом от 0% до 100% реализуется контуром «прямоугольник» плюс «палка-поперечина», которая делит наш прямоугольник на две половины. Заливку ставим для одной половины нашего прямоугольника. Параллельно сдвигаем «палку-поперечину», меняя соотношение площадей половинок – меняется и площадь заливки. Т.е. сделать заливку круга – та еще задача. Короче рисовать надо приноровится, а иногда и изголиться. А вот в Citect с такого рода заливками проблем нет.


Пример заливки в Citect

Нарисовать подобное в openSCADA целая задача.

Библиотечный виджет — sets – обработка




Вот мы и подошли к самому интересному – связыванию графики на странице (да и вообще на любом виджете) с тегами (атрибутами в терминах openSCADA). Для открытия вышеуказанного окна надо дать фокус нашему виджету и нажать кнопку с гаечным ключом . В открывшемся окне на вкладке Обработка на языке JavaLikeCalc пишем, собственно, всю обработку, создаем дополнительные переменные у виджетов, конфигурим переменные виджетов. Чтобы иметь возможно работать с атрибутом (в данном случае атрибут = переменная), надо у соответствующего атрибута поставить галку в столбце «Обработка».

Второй способ обратиться к атрибуту: vcaAttrGet(strParsePath(path,0)+"/pg_control/pg_ElCadr/a_pgOpen").
Третий способ: через объект this — this.nodeList(«pg_» ) означает получить список виджетов у главного виджета.
Во втором и третьем случае ставить галку «Обработка» не надо.

Обратите внимание на колонку «Конфигурационный шаблон» – это как раз та самая изюминка связывания с тегами, про которую я говорил ранее. Там вы задаете шаблон в виде «параметр|атрибут», где параметр и атрибут – сущности из модуля Сбор данных. И тогда на вкладке Связи нам достаточно привязать параметр (например, G1), а атрибуты свяжутся автоматически согласно заданным шаблонам. Если параметр содержит 100 атрибутов, сэкономите массу времени. Умелое проектирование с таким маневрированием позволяет делать весьма гибкие системы.
Связать можно не только с тегом из Сбора данных, но и с любым атрибутом виджета, чем я и пользуюсь.

Библиотечный виджет — general – связи




Как я уже говорил, связаться с тегом можно прямо из кода в обработке (SYS.DAQ.xxx и так далее вплоть до тега), а можно на вкладке Связи. Чтобы атрибут появился на вкладке Связи ему надо задать тип связи в колонке Конфигурация на вкладке Обработка:

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

Плюсик в конце строки-привязки говорит корректности пути.

Библиотечный виджет — sets — связи с атрибутом виджета




Пример связи переменных виджетов (например, PU_set_G1) с переменной основного виджета. На вкладке Обработка прописываешь шаблон, а тут только wdg:… копипастишь.

Библиотечный виджет — объект this




Пример работы с переменными виджета через объект этого виджета this.

Библиотечный виджет — vcaAttrGet




Тот самый третий способ обращения к переменным виджета через vcaAttrGet.

Библиотечный виджет — обработка событий клавиатуры/мыши




Открытие страниц в Runtime конфигурируется в специальном свойстве любого виджета Обработка событий. А вот обработка всех остальных клавишных (клавиатура, мышь) событий производится в Обработке.

Vision — таблица — редактирование




Графический элемент «таблица» создается а-ля HTML. На момент разработки описываемого проекта получить координаты «выделенный столбец: выделенная строка» было невозможно, вставить в ячейку объект типа поле ввода/флажок/кнопка невозможно. Может сейчас возможностей добавилось.

Тренды




Один холст может содержать до 99 перьев включительно (передаем привет Wonderware с ее 8 без доплаты).
А вот интерфейс взаимодействия с перьями приходится делать самому. Можно взять пример из демо-проекта, но там много ручной работы при конфигурировании. Для первого раза пойдет, но далее хочется более автоматизированного (из скрипта) интерфейса. Внешне подходит как Citect, но там таблица с флажками для управления видимостью перьев, а в openSCADA пока нет подходящего инструментария. Либо надо другую реализацию легенды придумать. Или на С++/Qt свою написать и в openSCADA добавить.

Механизм экспорта и печати


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


«Информационная панель в нижнем правом углу экрана»

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


Смена пользователя – выбор требуемого


Смена пользователя – ввод пароля

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



Список доступных для печати страниц:



То же самое и для экспорта.



Имя файла в поле должно содержать соответствующее расширение (*.png, *.xpm,*.jpg).
В случае выбора несуществующего на странице элемента появляется предупреждающее сообщение.

Страницы экспортируются в файлы изображений. Документы экспортируются в файлы web-страниц (*.html, которые можно открывать любым web-браузером и печатать из него) или текстовый файл (*.csv, который можно открыть в MS Excel или другом табличном редакторе для дальнейшей обработки). Документы в openSCADA — это встроенный базовый тип виджетов из которых можно создавать свои любые отчетные формы. Конфигурация а основе HTML и CSS.



Вывод: openSCADA — очень даже энтерпрайз.

UPD1

Передача параметров от элемента во всплывающее окошко


Например, у нас на странице есть графический элемент кран и кнему мы сделали всплывающее окошко (по щелчку мышкой на кране) с кнопками управления и полями ввода, и другой подробной информацией по крану (клапану или регулятору на нем). Краны у нас библиотечные элементы, каждый экземпляр привязан к «соим» тегам. Всплывающее окошко библиотечное, одно на все краны одного типа на странице, а то в проекте. Как нам передать во всплывающее окошка конкретные параметры-теги конкретного крана/клапана? Не знаю как это в WinCC делается — не делал, но в Citect это делается ушлепочно: в странице-окошке вместо тегов просываются плейсхолдеры вида ?1? или ?5?, а в кнопке вызова всплывающего окошка на кране прописывается встроенная в Citect функция замены плейсхолдера на конкретный тег — WinAss(-2,5,«DU_PID_kP_valve1»,0), где 5 — значит на место ?5? будет подставлен тег DU_PID_kP_valve1. Поскольку в Citect в окошке кода для кнопки ограничение на размер текста в 255 символов, то при большом количестве тегов на странице или каких-то «финтах ушами» типа объявления переменных или склеивания имени тега придется оформлять все это в отдельный скрипт (функцию) в редакторе скриптов CiCode Editor и в кнопке только вызвать соотвествующую функцию. По-моему даже в RSView32 это реализовано по-проще — на место плейсхолдера можно вставлять лубое число или текст чтобы в итоге получилось имя тега, скрипта, окна или чего другого.

В openSCADA никакого подобного огорода не надо: во всплывающем окошке используем те же теги, что и у крана, а в конфигурационном шаблоне атрибута (т.е. тега) всплывающей страницы пишем
<page>|DU_PID_kP
— что значит «возьми тег там же, где и кран берет» — ВСЁ!

Tags:
Hubs:
+24
Comments185

Articles