Реальные промышленные принтеры
Реальные промышленные принтеры

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

Контекст

Это было около 12-15 лет назад. Были определённые потребности у бизнеса и определённая свобода действий вместе с тем. Роль моя была чисто разработческая, так что ни на стек, ни на цели я не влиял. Все названия и имена нарочно заменены.

C4 Context

К тому времени уже был кое-какой опыт работы. Успел и побывать винтиком в крупном промышленном холдинге с офисами в исторической части города, и в (почти) никому не известной компании посреди промзоны с романтическим названием проезда (9062) и трёхзначными номерами строений.

В «Унисон-Экспресс» я пришёл, уже имея примерно трёхлетний опыт. Компания занимается промышленной печатью: огромные форматы бумаги, огромные тиражи и мощные заказчики (что-то вроде Россетей, МГТСов и разных министерств). Несколько тысяч сотрудников, большой офис, большое круглосуточное производство, многолетние контракты. Множество разных художников-дизайнеров-верстальщиков. Ну и, как положено серьёзному бизнесу – свой АйТи-департамент, в котором не только аналитик был (где-то), но и, поговаривали, даже тестировщик.

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

  • при поломках и сбоях следует вызывать дорогого и редкого импортного специалиста, причём не из Средней Азии, а из Средней Европы;

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

  • сроки поставки запчастей соответствуют трудоёмкости процесса в целом. Это почти срыв сроков исполнения заказов и проблемы с репутацией (заказчики надёжные «тяжеловесы», но и требуют тоже надёжности);

  • простой оборудования для руководства не намного лучше поломки: дорогая эксплуатация, большие потребности в энергии, поэтому должно работать если включено питание;

  • проприетарность и закрытость софта. Помните первую операционную систему от ксерокса? Ну вот тут та же самая история. Обновлять нужно, ибо требование гарантии, но не сотрудниками компании (см.выше). Что-то «доставить» на сервер оборудования невозможно (но есть стандартные настройки хуков, о них позже);

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

Situation

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

  • определить в моменте и ретроспективно: насколько обоснованно было загружать конкретными проектами конкретные единицы оборудования (не все же заказы срочные);

  • определить, а работало ли оборудование на регламентированных скоростях. Если скорость меньше заданной производителем штатной скорости – могло быть неэффективное распределение нагрузки, может, рабочие отлынивают от работы. Если больше рекомендуемой скорости – это резкое увеличение рисков поломки (что ещё хуже);

  • определить, насколько эффективно работают рабочие. У каждого в обслуживании было несколько промышленных принтеров, конвейеров или другой техники. Если большие промежутки в работе оборудования – может возникнуть вопрос, а чем занимался рабочий в 4 часовых перерыва;

  • в идеале, конечно, определить, а обоснованно раздутие штата иностранными специалистами;

  • как промежуточный вариант: определение средней загрузки рабочего и расчет премий тем, кто превышает среднее по цеху;

  • дополнительно можно было бы в едином интерфейсе наблюдать, какие проекты обрабатываются: на каких этапах обработки, на каком оборудовании выполняются, есть ли ошибки.

Просто конфигурированием серверов печати и оборудования в этой ситуации не обойтись:

  • оборудование разного типа, разных производителей;

  • у части оборудования есть нативный сервер печати Zylindr, на нём вся телеметрия, конфигурации, настройки и шедулеры, но невозможно поставить сторонний софт;

  • часть оборудования просто подключена к Ethernet;

  • часть оборудования работает с до-интернетовской эпохи, но, впрочем, имеет простые китайские датчики, которые тоже подключены к сети (отправляют телеметрию, например, считают проезжающие мимо них товары);

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

Кто и как следил за производством:

  • несколько ПК на производстве у ответственных товарищей (старшие смен и выше), с неторопливой сетью;

  • много менеджеров и директоров в бэк-офисе, с ПК и ноутбуками.

Зал на производстве примерно такой был. Лампы немного отличались
Зал на производстве примерно такой был. Лампы немного отличались

Task

Диагностика. Что мы видим?

С��ачала надо было понять, какие данные есть. Мыжпрограммисты.

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

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

Дальше была задача на основе телеметрии определить, с какой скоростью какое оборудование работает. Что уже было:

  • каждую секунду (N-секунд) сервер печати или датчик дает телеметрию. Это условно строковое сообщение, примерно в виде:

<дата-время>-<имя-устройства>-<счётчик>
  • печатное оборудование ещё дает привязку к проекту или файлу;

  • датчики и более простое оборудование только считают, но не знают что;

  • датчики и более простое оборудование не знают ни о каком http и, тем более, о базах, это слишком сложно для них.

На основе того, что хранилось, удалось по части оборудования рассчитать в пределах проекта или файла среднюю скорость:

<дата-время-1>-<имя-устройства>-<счётчик-1>
<дата-время-2>-<имя-устройства>-<счётчик-2>

velocity-<имя-устройства> = (<счётчик-2>-<счётчик-1>) / (<дата-время-2>-<дата-время-1>)

За каждую единицу времени рассчитывалась скорость, далее шла группировка в периоды работы единицы оборудования по одному проекту с похожими скоростями. В итоге получалась разбивка логов на промежутки работы с разными скоростями. Опытный производственник, видя имя устройства, определял на глаз, нормально ли работает техника или на «повышенных оборотах».

Этот прототип был в виде внушительных размеров SQL-скрипта c многократной вложенностью, позже получив статус view. Таким незатейливым решением я смог показать, что:

  • для части оборудования данных достаточно, их можно анализировать;

  • цифры получаются объективно похожие на правду;

  • несмотря на большие объемы телеметрии данные можно получить достаточно быстро.

Что реально нужно заказчикам и чего не существует?

Были и негативные результаты, которые и были причиной начала разработки проекта:

  • часть оборудования неотслеживаемая, надо прорабатывать хардверно-софтверные варианты;

  • по каждой единице оборудования надо учитывать штатный режим работы для правильного анализа;

  • надо автоматизировать рассылку отчётов по быстродействию директорам департаментов (кто-то переживает за оборудование, кто-то – за лентяев, кто-то следит за сроками и договорённостями);

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

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

C4 Containers

Виды оборудования

Итого получается, в зависимости от источника данных, два типа оборудования:

  • сервер печати Zylindr как чёрный ящик, за ним какое-то дорогое оборудование. На нём специфическая операционная система, с дорогой консультацией и с бесплатной отсутствующей документацией;

  • всё прочее, дешёвое, простое. Либо оборудование, либо датчики на нём по RJ-45 цепляются, куда-то что-то отдают то ли по TCP, то ли по UDP.

Имеющиеся ресурсы

Какой-то сервачок с условно-существующим сисадмином и:

  • с одной СУБД (Oracle);

  • возможностью хитрой командой пропинговать подключённые устройства;

  • возможностью что-то установить (с ограниченными ресурсами).

Человеческие ресурсы:

  • я, только прошедший испытательный срок;

  • пару дельфистов на других задачах;

  • начальник отдела, очень опытный и ответственный, но – см.выше;

  • где-то бизнес-аналитик, который в этой задаче не особо полезен;

  • директор ИТ-департамента, который в голове компилирует SQL, проверяя на правильность, потом запускает на своём компе и на основе данных тысячи строк определяет корректность скрипта (не шутка: смог обосновать, что видит, и почему считает правильным).

Action

В поисках ресурсов.

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

Когда обнаружили потребность собирать данные со всего оборудования стало понятно, что один ИТ-департамент не справится. Пригласили в помощь аналитика с производства. Борис Юрьевич тоже много лет работал на компанию, но знал не только цифры, но и дружил с «железом», мог подключиться почти к каждой дорогой железке почти по каждому протоколу.

Научно-исследовательская и опытно-конструкторская работа и программно-аппаратное решение.

Пояснение НИОКРа

Такими словами нас в шутку обзывали. По-английски никто в коллективе не говорил, и MVP не было.

Первое, что Борис Юрьевич сделал – правильно переподключил датчики, установил OPC сервер и включил сбор телеметрии с него. Для меня это было близко к магии: в одном приложении я увидел всё «неумное» оборудование. Ну заодно выяснили, что рабочие периодически отключают эти датчики, чтоб не наблюдали за режимом работы.

Дальше он, перекрестившись под личную ответственность влез в UI сервера Zylindr, нашёл массу интересных настроек на не совсем русском языке. После этого я получил (подозреваю, не полностью легальный) доступ к UI движка конфигурирования процессов и обнаружил там возможность создания триггеров. Из доступного там была возможность указания некоторых bash-скриптов (хотя ни проводника, ни CLI в явном виде в системе не обнаружили). Дальше уже на локальном компьютере написал скрипт упаковки данных в пакет и отправки по TCP на хост и порт нашего сервера. Как бы сказали: написал сериализатор и клиент. Конечно, в том чудном центрально-европейском недружелюбном интерфейсе весь стострочный скрипт пришлось вбивать заново (кто писал отправку пакетов в баше – поймёт меня). Интересно, что никакие политики или файерволы не помешали гонять по сети потенциально тайную информацию.

В IT-департаменте было всего несколько разработчиков, каждый прикреплённый к своему набору ПО, поэтому технологический стек выбирать не пришлось: Delphi или C++, РСУБД (SQL, plsql). Отдельные системы между собой или никак не общались, или через коммунальные схемы БД. Без автоматизации деплоя. Без каких-то специальных систем сбора логов.

Стало понятно, что для сбора данных с оборудования надо использовать какой-то новый канал. На тот момент в компании не было какого-то решения или экспертизы по использованию шин данных, поэтому остановились на первом попавшемся простом решении – протокол MQ в виде MQTT (для телеметрии), под который были и серверные решение, и клиентские, и какие-никакие библиотеки фрагменты кода под наш стек. Одна из имплементаций, RSMB, так и называется – «очень маленькая шинка». Для пробы написал несколько консюмеров на дельфи, даже с многопоточностью (2!). Один консюмер писал в базу, в которую же мы завели и технические справочники, найденные нашим экспертом Борисом Юрьевичем. Второй консюмер имел GUI, просто отображающий известные единицы оборудования и их статусы (частично статусы принимал в сообщениях, частично – рассчитывал). Это было начало функционала наблюдаемости.

И так, оборудование типа Zylindr отправляет по триггеру расширенную телеметрию на шину, в частности отображается (со статусами и доп.информацией) на дашборде, можно заняться прочим оборудованием. Как мы помним, где-то на сервер стоит в виде службы и клиента к ней OPC-сервер, очень старый проверенный временем. Как оказалось, он поддерживает такую штуку, как Оля (американцы пишут OLE). Это значит, что подключив в приложение стандартный компонент из дельфи (который, на самом деле, просто вызывает системную функцию Windows XP), можно быстро подружиться с сервером, сделать вид, что ты клиент. На самом деле очень просто. Осталось написать приложение, которое то ли по событию, то ли периодически залезает на этот сервер и берет скопом всю доступную там телеметрию. И приложение может писать в шину так, чтоб в шине телеметрия имела общий формат независимо от того, откуда получена.

Скучный скриншот консоли
Каждый датчик отдавал несколько значений (тегов), их можно было группировать в одно сообщение
Каждый датчик отдавал несколько значений (тегов), их можно было группировать в одно сообщение

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

C4 — Components

Проект без ��роекта

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

В плане реализации решили остановиться на общей шине данных и множестве модулей (программ, консолей, скриптов и прочее), которые общаются по этой шине и могут писать в БД. Создали пару схем в нашей любимой Оракле, распределили так, чтобы каждый модуль данные или писал, или читал, чтобы избежать всяческих блокировок. Не CQRS, но очень похоже.

Action (подробнее)

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

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

Пример запроса (в редакторе выглядит лучше, чем в статье)
with base_stats as (
/*статистика конвейеров не требует сбора статистики заранее*/
select
   decode(grouped,
                 0, event_from_d,
                 decode(work_status,
                             null, event_from_d,
                              1, (min(decode(gg.work_status, -1, to_date(:to_d, 'dd.mm.yyyy hh24:mi:ss'), work_start)) over (partition by line_n)), /*work_start*/
                             null)
                 ) as event_from_d
   , decode(grouped,
                 0, event_to_d,
                 decode(work_status,
                             null, event_to_d,
                             1, (max(decode(gg.work_status, -1, to_date(:from_d, 'dd.mm.yyyy hh24:mi:ss'), work_finish)) over (partition by line_n)), /*work_finish*/
                             null)
                 ) as event_to_d
   , log_parser.p_stat_perf.date_interval_to_string(
                       decode(grouped,
                               1, decode(work_status,
                                           null, (event_to_d - event_from_d),
                                           duration_sum),
                               (event_to_d - event_from_d))
                                               ) as duration_str
   , sensor_sum_cnt
   , round(
         decode(grouped,
                 1, decode(work_status,
                             null, sensor_sum_cnt / (event_to_d - event_from_d),
                             -1, 0,
                             sensor_sum_cnt / duration_sum),
                  decode(work_status,
                             -1, 0,
                             sensor_sum_cnt / (event_to_d - event_from_d)
                             )
                 ) / 24
         ) as prod_speed
   , prod_speed_min
   , prod_speed_max
   , least(min_sensor_cnt, max_sensor_cnt) as min_sensor_cnt /*для записей физ.простоя макс=предыдущ.зн.счетчика, мин= предыдущ.зн.сч.+1*/
   , max_sensor_cnt
   , decode(work_status,
                   -1, 'ПРОСТОЙ',
                    0, 'лог.простой',
                    1, 'в работе',
                   null) as work_status_name
   , nvl(work_status, 999) as work_status
   , equipment_name
   , work_status_grouped
   , row_number() over (order by gg.equipment_name, gg.threshold_speed,
                                decode(gg.grouped, 1, nvl(gg.work_status, 333), -2),
                                gg.event_from_d) as row_num
   , case
        when grouped = 1    then -14540253 --жирный шрифт на сером фоне
        when work_status = 0 then 12703735 --оранжевый
        when work_status = -1 then -8421631 --жирный шрифт на красном фоне
        else 0
    end as color
from
(--gg
    select
       min(event_from_d) as event_from_d
       , max(event_to_d) as event_to_d
       , sum(event_to_d - event_from_d) as duration_sum
       , greatest(sum(max_sensor_cnt - max_sensor_cnt_prev + 1), 0) as sensor_sum_cnt /*для физ.простоя*/
       , round(min(prod_speed_min)) as prod_speed_min
       , round(max(prod_speed_max)) as prod_speed_max
       , min(min_sensor_cnt) as min_sensor_cnt
       , max(max_sensor_cnt) as max_sensor_cnt
       , work_status
       , line_n
       , equipment_name
       , threshold_speed /*пороговая скорость работы*/
       , min(work_start) as work_start
       , max(work_finish) as work_finish
       , grouping(f3.gr) as grouped
       , grouping(f3.work_status) as work_status_grouped
    from
       ( /* f3 */
         select
             line_n
             , event_from_d
             , event_to_d
             , max_sensor_cnt_prev
             , min_sensor_cnt
             , max_sensor_cnt
             , work_status
             , prod_speed_min, prod_speed_max
             , ord_
             , equipment_name
             , sum(case when line_n_prev = line_n and work_status_prev = work_status then 0 else 1 end) over (partition by line_n order by ord_) as gr
             , threshold_speed /*пороговая скорость работы*/
             , decode(work_status, -1, event_to_d, event_from_d) as work_start
             , decode(work_status, -1, event_from_d, event_to_d) as work_finish
    from
        (   /*f2 table*/
          /* сглаживаем перепады простой-работа - восстанавливаем значения счётчиков и диапазоны */
        select
             line_n
             , decode(line_n - line_n_prev, 0, event_to_d_prev, event_from_d) as event_from_d
             , event_to_d
             , decode(work_status, -1, min_sensor_cnt, decode(line_n_prev - line_n, 0, max_sensor_cnt_prev + 1, min_sensor_cnt)) as max_sensor_cnt_prev
             , max_sensor_cnt_prev + 1 as min_sensor_cnt
             , max_sensor_cnt
             , work_status
             , prod_speed_min, prod_speed_max
             , ord_
             , equipment_name
             , threshold_speed /*пороговая скорость работы*/
             , (lag(work_status, 1, -999) over (order by line_n, equipment_name, event_from_d, work_status)) as work_status_prev
             , line_n_prev
    from
        (   /*f table*/
          /* сглаживаем перепады простой-работа - отсеиваем события малой длительности, которые физ.простой или после которых не следует физ.простой */
        select
             line_n
             , event_from_d
             , event_to_d
             , lag(max_sensor_cnt, 1, null) over (order by e.ord_) as max_sensor_cnt_prev
             , lag(event_to_d, 1, null) over (order by e.ord_) as event_to_d_prev
             , lag(line_n, 1, -999) over (order by e.ord_) as line_n_prev
             , row_number() over (partition by line_n, equipment_name order by e.ord_) as ord_
             , /*переопределяем стаус после склейки*/
             case
                         when work_status = -1
                              then -1 --физический простой
                         when (max_sensor_cnt - (lag(max_sensor_cnt, 1, null) over (order by e.ord_))) /
                                greatest(
                                         (event_to_d - (lag(event_to_d, 1, to_date(:from_d, 'dd.mm.yyyy hh24:mi:ss'))
                                                       over (order by e.ord_)
                                                      )
                                          ) * 24
                                 , 10/3600) < nvl(threshold_speed, 0)    /*:p_delta_min=10*/
                               then 0 --простой -idle
                         else 1       -- норм.режим
                    end as work_status
             , min_sensor_cnt
             , max_sensor_cnt
             , equipment_name
             , threshold_speed /*пороговая скорость работы*/
             , prod_speed_min, prod_speed_max
    from
        (-- e table, with filter by event duration
          select
             line_n
             , event_from_d
             , /*если эта строчка уровнем выше отсечётся - хоть запомнит td предыдущей ей строке (которая тоже может срезаться)*/
               case when (work_status <> -1 and nvl((lead(work_status, 1) over (partition by line_n, equipment_name order by event_from_d, work_status)), -1) = -1)
                                    /*если после этой строки (не физ.простой) - физ.простой*/
                             or (event_to_d - event_from_d) * 86400 > 300 /*:p_event_dur_min=300 sec*/
                             or (row_number() over (partition by line_n, equipment_name order by event_from_d, work_status) = 1)
                            /*потом уберем ord_=1 (именно такой ord_ определяется уровнем выше)*/
                     then event_to_d
                     else (lag(event_to_d, 1, to_date(:from_d, 'dd.mm.yyyy hh24:mi:ss'))
                            over (partition by line_n, equipment_name order by event_from_d, work_status)     /*такие строки помечаем для удаления!! to_be_deleted=1*/
                           )
               end as event_to_d
             , case when (work_status <> -1 and nvl((lead(work_status, 1) over (partition by line_n, equipment_name order by event_from_d, work_status)), -1) = -1)
                                  /*если после этой строки (не физ.простой) - физ.простой*/
                            or (row_number() over (partition by line_n, equipment_name order by event_from_d, work_status) = 1)
                            /*потом уберем ord_=1 (именно такой ord_ определяется уровнем выше)*/
                            or (event_to_d - event_from_d) * 86400 > 300 /*:p_event_dur_min=300*/
                    then 0
                    else 1
              end as to_be_deleted
             , min_sensor_cnt
             , max_sensor_cnt
             , prod_speed_min, prod_speed_max
             , lead(work_status, 1) over (partition by line_n, equipment_name order by event_from_d, work_status) as work_status_next
             , work_status
             , equipment_name
             , threshold_speed /*пороговая скорость работы*/
             , row_number() over (order by line_n, equipment_name, event_from_d, work_status) as ord_
          from(
            --dd готовые результаты, только без нумерации
            select
            line_n
          , min(log_fd) as event_from_d
          , max(log_td) as event_to_d
          , min(prod_speed) as prod_speed_min
          , max(prod_speed) as prod_speed_max
          , min(decode(curr_n_prev, 0, curr_n, curr_n_prev + 1)) as min_sensor_cnt
          , max(curr_n) as max_sensor_cnt
          , equipment_name
          , work_status
          , threshold_speed /*пороговая скорость работы*/
        from
           (  --table bb, group it!
        select
                    log_fd
                   , log_td
                   , prod_speed
                   , work_status
                   , sum(new_part2) over (order by line_n, log_fd, work_status) as gr
                    , line_n, line_n_prev, curr_n, curr_n_prev, ord_, log_delta, new_part, new_part2
                    , equipment_name
                    , threshold_speed /*пороговая скорость работы*/
                     , lvl /*debug*/
        from (--aa таблица
          select
                    log_fd, log_td
                      , decode(work_status - (lag(work_status, 1, 0) over (order by ord_)), 0, new_part, 1) as new_part2
                   , prod_speed
                   , work_status
                    , line_n, line_n_prev, curr_n, curr_n_prev, ord_, log_delta, new_part
                    , equipment_name
                    , threshold_speed /*пороговая скорость работы*/
                     , lvl /*debug*/
           from ( --f таблица
                select
                   log_fd, log_td
                   , new_part
                   , decode(curr_n_prev, 0, 0, (curr_n - curr_n_prev) / log_delta) as prod_speed
                   , case
                         when curr_n = curr_n_prev
                              then -1 --физический простой
                         when (curr_n - curr_n_prev) / log_delta < nvl(ep.value, 0)
                               then 0 --простой -idle
                         else 1       -- норм.режим
                    end as work_status
                    , line_n, line_n_prev
                    , curr_n
                    , curr_n_prev
                    , ord_, log_delta
                    , ep.equipment_name
                    , ep.value as threshold_speed
                     , lvl /*debug*/
                from ( --table e
                select
                   log_fd, log_td
                    , new_part
                    , line_n, line_n_prev
                    , curr_n + 1000000 * need_add_1000000 as curr_n
                    , decode(line_n - line_n_prev,
                                  0, lag(curr_n + 1000000 * need_add_1000000, 1, 0) over (order by ord_),
                                  0) as curr_n_prev
                    , ord_, log_delta
                     , lvl /*debug*/
                from (
                        --table d
                select
                    log_fd, log_td
                   , greatest((log_td - decode(curr_n, 0, log_td, log_fd)) * 24, 10/3600) as log_delta
                   , decode(line_n - line_n_prev, 0, 0, 1) as new_part
                  , line_n, line_n_prev
                  , curr_n
                  , sum(case
                              when curr_n < decode(line_n - line_n_prev, 0, curr_n_prev, 0)
                              then 1
                              else 0 end
                         ) over (order by ord_) as need_add_1000000
                  , ord_, lvl /*debug*/
              from
                (-- b2, добавляем записи физ.простоя, корректировка fd, td, учитываем добавленные записи физ.простоя
                  select
                          decode((lvl - 1) *
                             (lvl - (lag(lvl, 1, 0) over (order by line_n, log_td, lvl))),
                                 0, lag(log_td, 1, to_date(:from_d, 'dd.mm.yyyy hh24:mi:ss'))
                                       over (order by line_n, log_td, lvl),
                                 log_td - 10/86400
                             )
                           as log_fd
                     , decode(lvl,
                                 1, log_td - 10/86400,
                                 log_td) as log_td
                     , line_n
                     , decode((lvl - 1),
                                    0, (lag(curr_n, 1, 0) over (order by line_n, log_td, lvl)),
                                    curr_n) as curr_n
                     , line_n_prev
                     , decode((lvl - 1) *
                             (lvl - (lag(lvl, 1, 0) over (order by line_n, log_td, lvl))),
                                    0, (lag(curr_n, 1, 0) over (order by line_n, log_td, lvl))
                                   , (lag(curr_n, 2, 0) over (order by line_n, log_td, lvl)))
                              as curr_n_prev
                     , row_number() over (order by line_n, log_td, lvl) as ord_
                     , lvl /*debug*/
                from
                ( /* table b
                    удобная выборка для просмотра сырых логов
                     от сырых логов отличается наличием столбцов со значениями предыдущей строки*/
                   select
                      log_td
                      , lag(log_td, 1, to_date(:from_d, 'dd.mm.yyyy hh24:mi:ss')
                            ) over (order by line_n, log_td) as log_fd
                      , lag(line_n, 1, -999) over (order by line_n, log_td) as line_n_prev
                      , line_n
                      , curr_n
                   from (
                   -------------------------------------------------------------------------------------------
                   ------------- первичные логи   ------------------------------------------------------------
                        select
                             cfg.line_id as line_n,
                             cast(l.value as number(8)) as curr_n,
                             to_date(to_char(l.ts, 'dd.mm.yyyy hh24:mi:ss'), 'dd.mm.yyyy hh24:mi:ss') as log_td
                         from       logs_table l
                             join  equipment_master s on (s.code = l.source)
                             join  line_config cfg on (cfg.sensor_code = s.id)
                         where  l.log_date between to_date(:from_d, 'dd.mm.yyyy hh24:mi:ss') - 1/24 and
                                     to_date(:to_d, 'dd.mm.yyyy hh24:mi:ss')
                                and  l.status_flag = 192
                        order by l.source, l.log_date
                   ------------- первичные логи  -------------------------------------------------------------
                   -------------------------------------------------------------------------------------------
                    ) l
                ) b
                join (select level as lvl
                           from
                              (select 1 from dual minus select 0 from dual)
                            connect by level < 3) rec_generator
                            on (lvl = 2 or
                                  ((line_n = line_n_prev) and
                                     (b.log_td - b.log_fd) * 86400 > 30 /*добавляем записи о физ.простоях*/
                                  )
                                )
                 ) b2
                   ) d
                ) e
                  join equipment_params ep on (e.line_n = ep.equipment_id and
                                                                  ep.param_code = 'MIN_SPEED' and
                                                                  ep.param_category = 1 and
                                                                  ep.equipment_type = 4)
                   where log_td > to_date(:from_d, 'dd.mm.yyyy hh24:mi:ss')
                ) f
            ) aa
            ) bb
            where new_part = 0
              and (log_fd <> log_td)
             group by line_n, equipment_name, threshold_speed, work_status, gr
             order by line_n, equipment_name, min(bb.log_fd), work_status
            ) dd
            where (work_status in (0, 1)) or
                    ((dd.event_to_d - dd.event_from_d) * 86400 > 300) /*:p_event_dur_min=300 отсеиваем физические простои*/
            order by line_n, equipment_name, dd.event_from_d, work_status
           ) e
        where to_be_deleted = 0
        ) f
      ) f2
      ) f3
       group by line_n, equipment_name, threshold_speed, rollup(work_status, gr)
      ) gg
  where gg.grouped = 1
    and gg.event_to_d - gg.event_from_d > 0
)
select
  a0.equipment_name
 , a0.sensor_sum_cnt as sensor_cnt_all
 , a3.sensor_sum_cnt as sensor_cnt_work
 , a2.sensor_sum_cnt as sensor_cnt_slow_work
 , a1.sensor_sum_cnt as sensor_cnt_standby
 , a0.min_sensor_cnt as a0_min, a0.max_sensor_cnt as a0_max
 , a1.min_sensor_cnt as a1_min, a1.max_sensor_cnt as a1_max
 , a2.min_sensor_cnt as a2_min, a2.max_sensor_cnt as a2_max
 , a3.min_sensor_cnt as a3_min, a3.max_sensor_cnt as a3_max
 , a0.event_from_d as event_from_d_all, a0.event_to_d as event_to_d_all
 , a3.duration_str as duration_work
 , a2.duration_str as duration_slow_work
 , a1.duration_str as duration_standby
 , a0.prod_speed as prod_speed_all
 , a3.prod_speed as prod_speed_work
 , a2.prod_speed as prod_speed_slow_work
 , a1.prod_speed as prod_speed_standby
from base_stats a0
      left outer join base_stats a1 on (a1.work_status = -1 and a1.equipment_name = a0.equipment_name)
      left outer join base_stats a2 on (a2.work_status = 0 and a2.equipment_name = a0.equipment_name)
      left outer join base_stats a3 on (a3.work_status = 1 and a3.equipment_name = a0.equipment_name)
where a0.work_status = 999

Может и неидеальный код. Может, сейчас написал бы лучше. Но сейчас сейчас, а тогда было тогда. Главное – но на миллионах строк работал быстро. С развитием системы данных стало больше. Деградации так и не застал.

Чуть позже появилось десктопное приложение, в котором, задав временные границы и ограничения по цеху или оборудованию, можно было получить массив данных. В случае, если в данных (предположительно) не хватало чего-то – можно было без звонка e-mail на производство узнать о доступности оборудования благодаря графическому дашборду (первое время он запускался только у меня, так решили).

Dashboard GUI оборудования. Максимальное приближение к окну проводника. Немного романтическая стилистика воспоминаний.
Dashboard GUI оборудования. Максимальное приближение к окну проводника. Немного романтическая стилистика воспоминаний.

Для уточнения данных в отчётах, локализации «слишком медленной» или «слишком быстрой» работы завели справочник оборудования, который заполнили по данным от специалистов производства. UI справочника предполагалось передать им же, но не сразу, со временем. Таким образом, удалось сделать отчёт с подсветкой строк с периодами аварийного режима работы (то есть временными интервалами с повышенными рисками поломки) и периодами простоя (в том числе «полупростоя»). В отличие от проприетарного ПО сервера печати, этот справочник и этот отчёт охватывал всё оборудование независимо от производителя.

Оборудование и характеристики
Оборудование и характеристики
Менее романтическая картинка
Конечно же скорректирована ИИ.
Конечно же скорректирована ИИ.

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

Далее добавляли отчёты всё по новым и новым требованиям. Стали показывать пользователям как текущую статистику по работающему оборудованию в зависимости от его типа, так и преднастроенные отчёты: те данные, которые бизнес запрашивал чаще всего. В десктопном приложении я позже добавил варианты печати выборки в файл, по заранее заданным шаблонам. Использовал отличную библиотеку FastReport, которая позволяла по единому шаблону формировать как текстовые файлы (csv, xml, xlt, doc, txt), так и картиночные (pdf, jpg, png, etc.) – на любой вкус.

Десктопное приложение распространили по менеджерам, им начали пользоваться. Директора департаментов захотели каждое утро получать отчёт – мы с помощью средств операционной системы сервера настроили планировщик, отправляли стандартный отчёт в формате PDF на заранее заданный список e-mail.

Отчёт о текущих данных. Цветом (яркой сепией) выделяются аномальные участки работы. Конкретно здесь оборудование работало почти вхолостую.
Отчёт о текущих данных. Цветом (яркой сепией) выделяются аномальные участки работы. Конкретно здесь оборудование работало почти вхолостую.

Пока система была частично в опытно-промышленной эксплуатации на ограниченном числе бета-тестеров было время пообщаться с потенциальными пользователями и с заказчиками (стейкхолдерами). Это, в основном, осуществлял наш аналитик Борис Юрьевич.

Одно из интересных требований – уведомление в режиме реального времени о выходе из строя оборудования на основе телеметрии. Если на производстве можно было это как-то решить электроникой, то руководству из бэк-офиса такое решение не подошло бы. Но у нас уже был дашборд, отображающий текущие данные. На шину повесили ещё один программный модуль, который на основе промежутков между телеметрией по одной единицы оборудования определял момент отказа. Он в шину отправлял алерт по конкретному оборудованию. Дашборд принимал статусы полиграфического оборудования от сервера печати, а стал принимать ещё и от модуля алертов, показывая оборудование, от которого не поступало данных. Дополнительно дашборд стал показывать время последних полученных данных для того, чтоб примерно можно было увидеть длительность аварии.

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

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

Поскольку система стала многомодульная, её стали называть уже сервис-ориентированной, для обеспечения надёжности в каждый модуль, обрабатывающий данные на шине, добавили heartbeat – с некоторой небольшой периодичностью модуль сообщал в шину о своём статусе. Если долгое время нет статуса – это авария модуля, тоже можно отправить сигнал об аварии. С точки зрения шины и наблюдаемости не было разницы в отсутствии сигнала от оборудования или от программного модуля. Тот же самый дашборд адаптировали под отображение модулей, у него стало два режима работы.

Dashboard для оборудования и программных модулей, всё максимально близко к «оконной» стилистике.
Dashboard для оборудования и программных модулей, всё максимально близко к «оконной» стилистике.
Скучная реальность
И это тоже не оригинал. Текст чуть скорректирован ИИ, чтоб не нарушать ничего.
И это тоже не оригинал. Текст чуть скорректирован ИИ, чтоб не нарушать ничего.

Далее были проведены исследования на возможность использования шины для общего месседжера, для передачи небольших файлов. Всё оказалось возможным, был создан прототип маленькой аськи, которую можно запустить на любом рабочем месте. Она пряталась в трей, а при получении нотификации показывала окно на рабочем столе. Также, зная идентификатор другого клиента (COMPUTERNAME), можно отправлять короткие текстовые сообщения. Всё преобразовывалось в бинарный вид, дополнялось заголовком с идентификатором модуля и идентификатором получателя, и отправлялось в MQ. Это был просто прототип.

Result

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

После некоторого периода эксплуатации руководители стали смотреть отчёты. Получились интересные выводы.

Например, что в среднем 40% оборудование больше 50% времени работает на скоростях ниже оптимальных. Если увеличить скорость, то можно уменьшить срок изготовления товара и повысить довольство клиентов. А ещё за смену на 1 единице оборудования можно напечатать больше, то есть за смену задействовать меньше принтеров, а, возможно, и персонала.

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

В среднем количество производимой продукции различалось на разном печатном оборудовании, даже с учётом поправочного коэффициента на штатную скорость работы. А вот на конвейерных линиях резкого расхождения производительности не было. Эти данные тоже повод рассмотреть внедрения KPI на производстве.

Что мне удалось сделать примерно за год:

  • получать актуальные данные со всего оборудования на производстве;

  • вести в одном месте журнал всей телеметрии;

  • разработать почти десяток отчетов и форм, частично автоматизировать сбор статистики и отправку директорам предприятия;

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

  • реализовать механизм нотификации (e-mail и чат).

Что было начато, но я не успел сделать:

  • внедрить производственный чат, доделать в нём список контактов;

  • систему редактирования отчётов. Пользователи с определенными правами могли задать SQL-скрипт, описание UI панели фильтров, шаблон FastReport, данные для планировщика при необходимости; другие пользователи в том же десктопном приложении могли бы пользоваться не только отчетами, которые я внёс в БД, но и кастомными;

  • автоматизировать рестарт модулей при получении сигнала о падении;

  • автоматизировать архивирование или очистку данных; впрочем, при мне деградации не было: достаточен запас прочности у СУБД;

  • автоматизировать сбор и сброс логов хотя бы в ту же оракловую базу.

Вольная трактовка архитектуры
Вольная трактовка архитектуры

Надеюсь, эта историческая справка была интересна. Цель её – показать технологии прошлого, всё ещё работающие и кое-где востребованные. Она не раскрывает вопросы «зачем», часть из которых может быть ещё под NDA, а только рассказывает «как».

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А вам доводилось строить сложные системы без оркестраторов и виртуализации?
50%Да1
50%Нет1
0%Я посмотреть0
Проголосовали 2 пользователя. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А доводилось ли строить серверную часть на голой БД?
33.33%Да1
66.67%Нет2
0%Я посмотреть0
Проголосовали 3 пользователя. Воздержавшихся нет.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Был ли интересен экскурс в прошлое?
66.67%Да2
33.33%Частично1
0%Нет0
Проголосовали 3 пользователя. Воздержавшихся нет.