За да реализираме една SCADA система, най-напред трябва да помислим за комуникацията с различни видове PLC. На пазара за контролери се срещат най-различни производители, предлагащи уреди с най-различни характеристики и десетки комуникационни протоколи. В почти всички случаи данните, които трябва да четем от контролера са малко по обем, в сравнение с паметта на съвременните компютри. Традиционно комуникацията с контролерите е трудна, понеже те могат да бъдат разпръснати на голяма територия. Този проблем е довел до използването през годините на най-различни комуникационни протоколи и преносни среди – голяма част от които изглеждат доста глупаво от съвременна гледна точка. Но тъй като в повечето случаи става дума за производствени процеси – на никой не му и минава през ума да променя каквото и да било. Тоест, принудени сме да работим при тези условия. Когато започнем работа, всякакви предположения за марката контролер и комуникационния протокол, след време ще се окажат неверни (по законите на Мърфи). Можем да се сблъскаме с всичко – от 9600 bps RS232 връзка, с ASCII протокол, до ISO върху Industrial Ethernet. Често пъти библиотеките за комуникация с контролерите не са с отворен код (за щастие при мен случая не е този). Можем да се надяваме, че те предлагат някакъв C интерфейс, който ние да ползваме от някакъв външна програма. Една от първите ни задачи е да реализираме универсален интерфейс към контролерите, който да ни позволява да четем и пишем в тях, без значение каква марка са и какъв протокол използват. Лично аз, дълго време живеех с убеждението, че е най-добре просто да използвам асинхронна комуникация с event loop. Това щеше да работи, ако “драйверите” за различните видове контролери се държаха добре и не променяха настройките на подаваните им файлови дескриптори. За съжаление, това е прекалено оптимистично предположение. Повечето контролери са ужасно бавни, в сравнение със съвременните процесори. Данните могат да се точат “дълго време”, а буферирането в ядрото може да забави получаването им от програмата. Понеже комуникацията се очаква да бъде в “реално време”, а реализацията на различните специфични протоколи върху TCP/IP например, е сложна – можем да очакваме, че голяма част от тези драйвери ползват блокираща, синхронна комуникация. Очевидно блокирането на event loop при отпадане на контролер не е добра идея (особено ако работим със стотици контролери). След обстоен анализ на ситуацията и кратка справка с някои исторически системи, аз се спрях на Reactor pattern за решаване на този проблем. Тъй като искаме да пишем на Ruby, нашата Reactor pattern реализация е EventMachine. За да я ползваме, трябва да напишем Ruby bindings за драйверите на контролерите. Това е доста досадна и неблагодарна работа, но за щастие разполагаме с Extending Ruby главата от Programming Ruby. След няколко дни писане имаме работещи Test::Unit::TestCase(s), които събират данни от контролерите. Как обаче да използваме нашия binding с EventMachine? В общия случай имаме клас PLC, в който има един гаден метод, например fetch, реализиран на C, който може да блокира. EventMachine ни дава метод defer, той приема два аргумента – процедури. Първата процедура е блокираща, тя се изпълнява на отделен thread в реактора, а втората е callback – извиква се с резултата от изпълниение на първата процедура (изпълнява се извън реактора). EventMachine ни дава възможност да работим с нишки индиректно, като по този начин си спестяваме всичките проблеми свързани с директното им използване. Ето една примерна конструкция с defer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
Естествено, за да работи всичко това, блокиращата операция трябва да има разумен timeout. Той зависи силно от вида контролер, комуникационния протокол и наблюдавания производствен процес. При мен този timeout е от порядъка на 5 секунди. Тъй като EventMachine е реализиран на C/C++, нашия драйвер за контролера също е писан на C, можем да очакваме, че закъсненията от използването на Ruby – динамичен език от високо ниво, са минимални. Гаранции за това обаче няма, много е важно всички компоненти да бъдат тествани много подробно, преди да бъдат пуснати в експлоатация. Официално, системата която аз разработвам се пише на Ruby, но реално зад всеки един ред код на Ruby стоят между 10 и 100 реда код на C. За съжаление, това в случая е неизбежно. Прочетените от контролерите данни отразяват състоянието на реалните обекти, които участват в производствения процес. Връзката между обекти и контролери е основополагаща за всички подобни системи – важно е да имаме максимално гъвкави методи за описание на тази връзка. За целта използваме DSL, който лесно може да бъде реализиран на Ruby. След като имаме добре дефинирани обекти, техните състояния и събитията, които водят до промяната им, можем да започнем да мислим за визуализацията им. За подобна система, би било много глупаво да не използваме Web-базирана технология. Тъй като изображенията на обектите са инженерни чертежи, които трябва да бъдат скалируеми, не можем да ползваме нищо друго, освен SVG. Във всяко едно SVG изображение има определено количество графични обекти. Всеки графичен обект може да бъде в едно от няколко изброени състояния, което се променя във времето. За да визуализираме графиката използваме WebKit прозорец. Този прозорец се създава с помощта на QtRuby, като за целта (леко) се променя WebKit конфигурацията. За да следим измененията на състоянията на обектите във времето трябва да използваме, някакъв вид асинхронна комуникация с WebKit. Тъй като събитията се случват на сървъра, използването на XMLHttpRequest заявки (по-известни като AJAX) не е достатъчно. Клиентът няма как да знае кога дадено събитие се е случило, за да пусне заявката. Разбира се, можем да ползва polling, но е далеч по-елегантно комуникацията да бъде асинхронна. Това което ни трябва е известно, като Reverse AJAX, HTTP Server Push или Comet. За да реализираме каквото и да било Web-приложение, трябва да имаме HTTP сървър. Естествено, той може да бъде нещо съвсем отделно и външно за системата, но тъй като така или иначе, ние имаме работещ EventMachine event loop, че даже и реактор, за комуникация с контролерите – ориентираме се към търсене на EventMachine-базиран HTTP сървър. Както може да се очаква, някой преди нас е написал такъв, казва се Thin. Можем ли да използваме Thin и EventMachine за Comet комуникация с WebKit? Върху този върпос е размишлявал и автора на Thin. Той е написал един Ruby gem, който се казва
Pusher и върши точно това което ни трябва. Благодарение на тези технологии, можем да обединим функционалността на HTTP сървъра, Comet сървъра, и модула за комуникация с контролерите. Разбира се, това е просто възможност за интеграция, в някои случаи не е разумно да се прави. Възможно е да “сглобяваме” SVG документите динамично, при всички случаи е трябва да имаме някакво Web приложение на сървъра, което да обслужва заявики към различни URL адреси. Най-удобно за целта можем да използваме Sinatra, или още по-добре Async Sinatra. Вече можем да доставяме SVG документите до клиентите, и да правим асинхронен “push” на съобщения към тях. Това което ще им изпращаме може да бъде XML или JSON съобщение (всъщност може да е всичко). Но най-ефективно от страна на браузера се обработват JSON съобщения, понеже те са просто сериализирани JavaScript обекти. При получаване на JSON съобщение, някакъв JavaScript в браузера трябва да намира елемента от документа, към който се отнася съобщението и да сменя някакви негови атрибути (или да го заменя с друг елемент). В HTML документите тези неща стават много лесно, благодарение на DOM стандарта, както и популярните JavaScript библиотеки от сорта на Prototype, Dojo и т.н., които го разширяват. Тези библиотеки не са много наясно с SVG и различните негови елементи, които могат да се срещнат в документа. Спасението в този случай беше библиотеката Raphaël, тя разширява DOM за SVG документи.
Мисля, че преди време Кнут беше казал, че съвременното програмиране все повече прилича на сглобяване на пъзел. В работата си по този проект, отделих адски много време за проучване на различни, вече реализирани библиотеки и технологии и много малко време за същинско писане на код. Но това не е задължително да бъде лошо.