How to / Sterowanie i akwizycja danych

Wykorzystanie mechanizmu CORS w modułach bezprzewodowych serii WISE

14.12.2016 Producent: Advantech Zastosowanie: Zakłady przemysłowe, Inteligentne miasta
Wizerunek autora
Jarosław Molenda Inne artykuły tego autora
Wykorzystanie mechanizmu CORS w modułach bezprzewodowych serii WISE

Miniaturowy serwer WWW wbudowany w moduły kontrolno-pomiarowe produkcji Advantech (te wyposażone w interfejs sieciowy Ethernet lub WiFi, czyli należące do serii ADAM-6000/6200 i WISE-4000) może stanowić w wielu przypadkach bardzo przydatny dodatek pozwalający na coraz częściej wymagane przez użytkowników zdalne udostępnianie danych procesowych za pośrednictwem przeglądarki. W rachubę wchodzi tu zarówno skorzystanie z wgranego fabrycznie uniwersalnego layout'u (obejmującego wszystkie we/wy modułu), jak i możliwość dostosowania go do konkretnej aplikacji, polegająca w skrócie na stworzeniu własnego pliku HTML z odpowiednim kodem JavaScript i wgraniu do pamięci modułu.

Realizacja zadania jest więc trywialna, ale tylko pod warunkiem, że wszystkie interesujące nas sygnały „obsługiwane” są przez ten jeden konkretny moduł, z którego serwera WWW będziemy korzystać (do którego na wstępie będziemy musieli zalogować się, po czym przeglądarka, korzystając z uzyskanego jednosesyjnego ciasteczka, będzie w stanie wykonywać odpowiednie zapytania REST).

Niestety temat komplikuje się w przypadku większej ilości, różnych rodzajów czy chociażby znaczącego rozproszenia sygnałów, gdzie nie istnieje inne sensowne rozwiązanie jak zastosowanie kilku modułów we/wy. I tu z pomocą przychodzi nam wsparcie dla mechanizmu CORS (Cross-origin resource sharing), zaimplementowane na chwilę obecną w modułach WISE-4000, pozwalające na stworzenie strony HTML z JavaScript’em pobierającym dane z innych urządzeń/adresów IP. Dla łatwiejszego zrozumienia zagadnienia rozważmy prosty czysto hipotetyczny (!) projekt:

Zadanie
Monitoring serwerowni. Kontroli (całodobowa archiwizacja plus zdalny wgląd w aktualny stan) podlegać powinny:
- temperatura otoczenia i wilgotność w pomieszczeniu
- stan pracy (on/off) wentylatora oraz czujnika otwarcia drzwi szafy serwerowej
- sygnał obecności człowieka w pomieszczeniu

Sygnały
Do obsłużenia mamy: dwa sygnały analogowe (z przetworników temperatury i wilgotności), jeden sygnał binarny z czujki obecności i dwa sygnały binarne z samej szafy. Łącznie 2AI i 3DI. Z powodzeniem zastosować możemy dwa moduły: WISE-4012 i WISE-4060.

Archiwizacja
Moduły wspierają komunikację w protokole Modbus/TCP, co pozwala tu na zastosowanie dowolnego urządzenia wyposażonego w obsługę tegoż protokołu. Z oferty Advantech na myśl przychodzi zastosowanie dowolnego panelu operatorskiego serii WOP z interfejsem sieciowym (N), który w razie takiej potrzeby mógłby dodatkowo pełnić rolę urządzenia autoryzującego wejście do serwerowni. Ale przecież nie to jest głównym tematem naszych rozważań...

Budujemy plik HTML
Podobnie jak w przypadku modułów serii ADAM-6xxx dane pobierać będziemy za pomocą AJAX'owych żądań dotyczących zaimplementowanych zasobów REST. Różnica polegać będzie na tym, że:
- żądania te będziemy kierować pod konkretne (różne) adresy IP (poszczególnych modułów)
- wszystkie nasze żądania będą zawierały dane autoryzacyjne (zamiast dołączanego ciasteczka sesji)
- formatem wymiany danych będzie tu json (a nie jak dotychczas XML)

Zaczynamy od elementu prezentacji danych. Możemy oczywiście zbudować nawet dowolnie skomplikowaną grafikę, ale tu dla uproszczenia posłużymy się prostą tabelą z danymi. W każdym z wierszy będzie to: jedna komórka z opisem danego parametru oraz druga - z odczytaną zawartością, koniecznie ze zdefiniowanym unikalnym identyfikatorem obiektu pozwalającym na wpisywanie danych przez JavaScript . Szkielet tej tabeli, z dodatkowym miejscem na wyświetlanie ewentualnych błędów mogłyby więc wyglądać następująco:

...
<table>
<tr><th>Temperatura<br>otoczenia</th><td id="room_temperature"></td></tr>
<tr><th>Wilgotność<br>względna</th><td id="room_humidity"></td></tr>
<tr><th>Obecność człowieka<br>w pomieszczeniu</th><td id="room_presence"></td></tr>
<tr><th>Wentylator</th><td id="rack_fan"></td></tr>
<tr><th>Drzwi szafy</th><td id="rack_door"></td></tr>
</table>
<div id="debug"></div>
...

 
W następnej kolejności definiujemy tablicę, która będziemy się posługiwać przy pobieraniu i wyświetlaniu danych. Wypełniamy ją odpowiednimi wartościami dla obydwu wykorzystywanych modułów, zawierającymi koniecznie:
- adres IP modułu
- dane autoryzacyjne z których zbudowany zostanie (wg wzorca "Basic: base64encode(user:password)" ) ciąg znaków dołączany do żądań AJAX
- ścieżkę (lub ścieżki) do zasobów REST
- identyfikatory powiązanych obiektów do prezentacji danych (czyli komórek w naszej zbudowanej w poprzednim kroku tabeli)
     - z danymi dotyczącymi skalowania, zaokrąglania i jednostki miary dla wielkości analogowych
     - z danymi dotyczącymi wyświetlania tekstowego sygnałów binarnych (będzie to czytelniejsze niż suche 1/0 lub on/off)

Dvc[0] =
 {
  module: 'WISE-4012',
  addr: 'http://xxx.xxx.xxx.xxx/',
  auth: MakeBasicAuth('xxxx','xxxxxxxx'),
  ai_path: 'ai_value/slot_0',
  ai_map:
   {
    0: {id: 'room_temperature', unit: '&deg;C', scale: {X1: 0, X2: 10, Y1: -30, Y2: 100}, digits: 1},
    1: {id: 'room_humidity', unit: '%', scale: {X1: 0, X2: 10, Y1: 0, Y2: 100}, digits: 0}
   },
  timeout: 500
 };

Dvc[1] =
 {
  module: 'WISE-4060',
  addr: 'http://xxx.xxx.xxx.xxx/',
  auth: MakeBasicAuth('xxxx','xxxxxxxx'),
  di_path: 'di_value/slot_0',
  di_map:
   {
    0: {id: 'room_presence', on: '<div class="warning">Tak!</div>', off: 'Nie'},
    1: {id: 'rack_fan', on: 'Włączony', off: '<div class="warning">Wyłączony!</div>'},
    2: {id: 'rack_door', on: '<div class="warning">Otwarte!</div>', off: 'Zamknięte'}
   },
  timeout: 500
 };


Jak widać wartość mierzona z kanału zerowego (w zakresie napięciowym 0÷10V) modułu 4012 ma być wyświetlana po przeskalowaniu do zakresu temperatur -30÷100°C i zaokrągleniu do jednego miejsca po przecinku. A stan wejścia cyfrowego z kanału drugiego (Di2) modułu 4060 ma być zamieniona na ciągi znaków "Zamknięte" i "Otwarte" (a ten drugi dodatkowo z ostrzeżeniem w postaci wyróżnienia kolorem (class)). Itd.

Dysponując tymi danymi przechodzimy do sedna sprawy:
- funkcji GetIos() inicjującej komunikację z modułami, pierwszy raz wywoływanej w "body onload="..."
- funkcji GetIo(dvno,io_type) wysyłającej cykliczne żądania AJAX dotyczące danych zasobów REST
- funkcji IoError(dvno,io_type,error) sygnalizującej wystąpienie błędu odczytu (debug i oznaczenie wszystkich powiązanych wartości jako N/A)
- funkcji IoDisplay(dvno,io_type,response) wyświetlającej dane w naszej tabeli

function GetIos()
 {
  GetIo(0,'ai');
  GetIo(1,'di');
 }

function GetIo(dvno,io_type)
 {
  var xmlHttp=null;
  if (window.XMLHttpRequest) xmlHttp=new XMLHttpRequest(); else xmlHttp=new ActiveXObject('Microsoft.XMLHTTP');
  xmlHttp.onreadystatechange=function() {if (xmlHttp.readyState==4)
   {if (xmlHttp.status==200) IoDisplay(dvno,io_type,xmlHttp.responseText); else IoError(dvno,io_type,xmlHttp.status);
    setTimeout(function(){GetIo(dvno,io_type)},1000);}}
  xmlHttp.open('GET',Dvc[dvno].addr+(io_type=='di'?Dvc[dvno].di_path:'')+(io_type=='ai'?Dvc[dvno].ai_path:''),true);
  xmlHttp.timeout=Dvc[dvno].timeout;
  xmlHttp.setRequestHeader('Authorization',Dvc[dvno].auth);
  xmlHttp.withCredentials=true;
  xmlHttp.send(null);
 }

function IoError(dvno,io_type,error)
 {
  AddHTML('debug','<b>IoError</b>("'+Dvc[dvno].module+'","'+io_type+'")='+error+'<br>');
  if (io_type=='di') for (var index in Dvc[dvno].di_map)
   SetHTML(Dvc[dvno].di_map[index].id,'<div class="warning">N/A</div>');
  if (io_type=='ai') for (var index in Dvc[dvno].ai_map)
   SetHTML(Dvc[dvno].ai_map[index].id,'<div class="warning">N/A</div>');
 }

function IoDisplay(dvno,io_type,response)
 {
  if (io_type=='di')
   {
    var io=JSON.parse(response).DIVal;
    for (var i=0;i<io.length;i++) if (Dvc[dvno].di_map[i])
     {
      var val=io[i].Val;
      SetHTML(Dvc[dvno].di_map[i].id,val?Dvc[dvno].di_map[i].on:Dvc[dvno].di_map[i].off);
     }
   }
  if (io_type=='ai')
   {
    var io=JSON.parse(response).AIVal;
    for (var i=0;i<io.length;i++) if (Dvc[dvno].ai_map[i])
     {
      var scale=Dvc[dvno].ai_map[i].scale;
      var val=scale.Y1+(io[i].EgF/1000-scale.X1)/(scale.X2-scale.X1)*(scale.Y2-scale.Y1);
      SetHTML(Dvc[dvno].ai_map[i].id,val.toFixed(Dvc[dvno].ai_map[i].digits)+Dvc[dvno].ai_map[i].unit);
     }
   }
 }


I to już praktycznie wszystko. Po minimalistycznym ubraniu kodu w kilka klas (css) otrzymujemy działający, niespełna pięciokilobajtowy plik html z dodatkowym skryptem basic-auth-lib.js odpowiedzialnym jedynie za wyznaczenie ciągu autoryzacyjnego (co tak naprawdę na upartego można wykonać "ręcznie" i wpisać w pełnym brzmieniu w konfiguracji Dvc()).

Gdzie wgrywamy nasz plik?
Generalnie gdzie chcemy. Tym razem na pewno nie do modułów, bo akurat te takiej możliwości nie dają. Ale tak naprawdę nie ma to większego znaczenia - bo wszystko wykonywane jest tu przez JavaScript po stronie klienta (przeglądarki), z odwołaniem pod konkretne adresy IP (które muszą być oczywiście dostępne dla hosta odpytującego) . Tak więc może to być zarówno plik umieszczony na jakimś serwerze www, jak i plik odczytywany lokalnie, z dysku komputera czy nawet karty pamięci smartfona. I o to właśnie chodziło :)

I oczywiście nic nie stoi na przeszkodzie temu, żeby nasz webowy interfejs rozszerzyć o obsługę wyjść cyfrowych. W tym celu do funkcji odczytu i do tablicy konfiguracji modułów należałoby dodać obsługę zasobów DOVal, a do ustawiania stanu danego wyjścia dopisać funkcję wysyłającą żądanie wykorzystujące metodę PUT, z podaniem (w content) nowego stanu wyjścia, także w formacie json, czyli w postaci ciągu {"Val":"0"} lub {"Val":"1"}.