W dzisiejszym wpisie przedstawię jak graficznie zilustrować informacje odbierane z poszczególnych urządzeń przy pomocy uScady.
Plik użytego JS wraz z zapisem licencji znajduję się w pliku gauge.min.css
Soft >= v1.00
Urządzenia wykorzystane do wizualizacji:
– LANtick: wejścia, wyjścia
– Nano Temp: temperatura
– Lumel N30P: napięcie, moc
Wykorzystane elementy
1. uScada
2. Nano Temp
3. Lantick 4-4
4. Miernik Lumel N30P
5. Zasilacze PoE
6. Switch i patchcord’y (kable LAN)
Schemat podłączenia
Zasada działania wraz z konfiguracją
W omawianym przykładzie moduł μScada (Master) odpytuje po Modbus’ie TCP wartości z adresów rejestrów odebranych od modułu Nano Temp (Slave), który przez przyłączony czujnik wysyła aktualną wartość temperatury powietrza, z modułu LANtick 4-4 (Slave) zostają odbierane stany wejść oraz wyjść, wraz z możliwością zmiany stanu wyjść. Został również dołączony miernik Lumel N30P (Slave) odpowiadający za pomiar napięcia i mocy pobieranej przez urządzenie. Miernik posiada zaimplementowany protokół komunikacyjny Modbus RTU. Do pamięci μScady zostały zapisane odpowiednie pliki i napisane odpowiednie skrypty.
Konfiguracja
Na początku należy załączyć MODBUS TCP w modułach: Nano Temp oraz LANtick.
Fabryczne dane logowania do Nano Temp i LANtick:
user: admin
hasło: admin00
W μScada zaczynamy od konfiguracji pliku POINTS.xml (w instrukcji zostało zawarte jak go znaleźć i co oznaczają poszczególne tagi).
Zaczynamy od zdefiniowania ilości grup punktów. W naszym przypadku jest to 5 grup. Odpowiednio:
1. Grupa punktów dla odczytu napięcia.
Definiujemy nazwy widoczne w pliku comm.xml:
– nazwę grupy punktów <id> np. Miernik_n
– numer od którego rozpoczynamy numerowanie <id_start> np. 1
– ilość punktów w grupie <len> np. 2
Z uwagi na to, że w dokumentacji miernika widnieje zapis, że:
– wybieramy protokół <protocol> dla modułu N30P- RTU
– adres fizyczny Modbus <dev_addr> np. 1
– ustawienie komendy <cmd> MB_HOLD
– ustawienie adresu rejestrów <address> – należy sprawdzić w dokumentacji, jakie adresy odpowiadają za napięcie- 7019
Należy pamiętać, że niekiedy adresacja w Modbus jest przesunięta o 1. Tak jest w mierniku Lumel N30P.
Dzieje się tak, ponieważ adresy rejestru modbus zaczynają się od 1, a adresy w ramce danych od 0. Jest to częsta przyczyna powstawania błędów w czasie programowania.
– typ dostępu <access> np. r – tylko odczyt
2. Grupa punktów dla odczytu mocy.
– nazwę grupy punktów <id> np. Miernik_p
– numer od którego rozpoczynamy numerowanie <id_start> np. 1
– ilość punktów w grupie <len> np. 2
– wybieramy protokół <protocol> dla modułu N30P- RTU
– adres fizyczny Modbus <dev_addr> np. 1
– ustawienie komendy Modbus <cmd> MB_HOLD
– ustawienie adresu rejestrów <address> – należy sprawdzić w dokumentacji, jakie adresy odpowiadają za moc- 7023
– typ dostępu <access> np. r – tylko odczyt
3. Grupa punktów dla odczytu stanu wejść.
– nazwę grupy punktów <id> np. Miernik_p
– numer od którego rozpoczynamy numerowanie <id_start> np. 1
– ilość punktów w grupie <len> np. 4 (z uwagi na 4 wejścia)
– wybieramy protokół <protocol> dla modułu LANtick- TCP
– adres fizyczny Modbus <dev_addr> np. 1
– ustawienie komendy Modbus <cmd> MB_COIL
– ustawienie adresu rejestrów <address> – należy sprawdzić w dokumentacji, jakie adresy odpowiadają za odczyt wejść- 1004
W przedstawionych modułach Inveo adresy Modbus zostały ujednolicone i nie są przesunięte.
W LANtick 4-4 wyjścia są numerowane jako 1, 2, 3, 4, a wejścia 5, 6, 7, 8.
– typ dostępu <access> np. r – tylko odczyt
– adres IP urządzenia, które odpytujemy <ip_addr> (można go odpowiednio ustawić)
– port ip- 502
4. Grupa punktów dla odczytu i ustawienia stanu wyjść.
– nazwę grupy punktów <id> np. LanTick_Wyj_nr_
– numer od którego rozpoczynamy numerowanie <id_start> np. 1
– ilość punktów w grupie <len> np. 4 (z uwagi na 4 wyjścia)
– wybieramy protokół <protocol> dla modułu LANtick- TCP
– adres fizyczny Modbus <dev_addr> np. 1
– ustawienie komendy Modbus <cmd> MB_COIL
– ustawienie adresu rejestrów <address> – należy sprawdzić w dokumentacji, jakie adresy odpowiadają za odczyt wyjść- 1000
– typ dostępu <access> np. rw – odczyt i zapis
– adres IP urządzenia, które odpytujemy <ip_addr> 192.168.111.60
– port ip- 502
5. Grupa punktów dla odczytu temperatury.
– nazwę grupy punktów <id> np. Temperatura_
– numer od którego rozpoczynamy numerowanie <id_start> np. 1
– ilość punktów w grupie <len> 1
– wybieramy protokół <protocol> dla modułu Nano Temp- TCP
– adres fizyczny Modbus <dev_addr> np. 1
– ustawienie komendy Modbus <cmd> MB_MULTIHOLD
– ustawienie adresu rejestrów <address> – należy sprawdzić w dokumentacji- adres rejestru 4004 odpowiada za temperaturę x10
– typ dostępu <access> np. rw – odczyt i zapis
– adres IP urządzenia, które odpytujemy <ip_addr> 192.168.111.15
– port ip- 502
Gdy wszystkie grupy zostały zdefiniowane należy uruchomić ponownie moduł, aby zmiany zostały zaktualizowane.
W celu sprawdzenia, czy wszystko przebiegło pomyślnie można sprawdzić plik comm.xml. W przeglądarkę wpisujemy adresIPurządzenia/comm.xml.
Zaznaczone linie dają nam informacje ile sekund wcześniej nastąpiło poprawne odpytanie z Modbusa. Jeżeli czas od ostatniej odpowiedzi cały czas rośnie (40-50 sekund) trzeba poprawić plik POINTS.xml, bądź sprawdzić dane urządzenie czy jest poprawnie podpięte.
Kolejny krokiem jest wgranie odpowiednich skryptów i plików obrazów do katalogu WWW.
Na koniec zajmujemy się edycją pliku index.htm.
W omawianym przykładzie zdefiniowaliśmy odnośniki do poszczególnych skryptów i zmienne w sekcji nagłówkowej.
Następnie zdefiniowaliśmy zawartość strony.
Na samym początku są zapisane informacje odnośnie stylu nagłówka i samego tekstu.
Elementem <canvas> dostosowujemy wygląd miernika, podpis, wartości max i min, podziałki, opóźnienia, kolory itp.
id=gauge3 odpowiada za wskaźnik dotyczący pomiaru mocy
id=gauge2 odpowiada za wskaźnik dotyczący pomiaru napięcia
id=gauge1 odpowiada za wskaźnik dotyczący pomiaru temperatury
Poniżej zdefiniowanego odczytu temperatury rozpoczyna się definicja wyglądu przycisków informujących o stanie wyjść oraz zdarzeniach występujących po ich naciśnięciu. W tym przypadku po naciśnięciu przycisku zostaje wywołana funkcja sendData(nr wejścia, stan na jaki ma zostać ustawiony przekaźnik (wł/wył)).
Ostatnim elementem wizualnym są ikony informujące o stanie wejść LANtick.
Teraz zajmiemy się wykorzystanymi funkcjami:
Funkcja updateStatus zajmuje się pobraniem danych z pliku xml’a. W pętli for sprawdzamy tablice lantick1 odpowiadającą za stan wyjść i określającą jaki obrazek ma zostać wyświetlony (0-OFF, 1- ON). Kolejna pętla odpowiada za wejścia.
var t1 = parseInt(getXMLValue(xmlData, 'Temperatura_1')); temperature = t1/10;
Powyższy kod odpowiada z pobranie wartości temperatury z pliku comm.xml i podzielenie jej przez 10, aby otrzymać interesującą nas wartość.
W tym momencie ominiemy linie kody dotyczące napięcia i mocy, aby wpierw wytłumaczyć występującą w nich funkcję “function ieee32ToFloat(intval)”. Funkcja to odpowiada za konwersję wartości z ieee (32bity) na wartość float- zmiennoprzecinkową. Dzięki temu otrzymujemy znane dla człowieka wartości napięcia i mocy.
Wracając do ominiętych linii kodu:
var v1 = parseInt(getXMLValue(xmlData, 'Miernik_n1')); var v2 = parseInt(getXMLValue(xmlData, 'Miernik_n2')); var v = v2 + v1*65536; var f = ieee32ToFloat(v); voltDeviation = f.toFixed(2); v1 = parseInt(getXMLValue(xmlData, 'Miernik_p1')); v2 = parseInt(getXMLValue(xmlData, 'Miernik_p2')); v = v2 + v1*65536; f = ieee32ToFloat(v); power = f.toFixed(2);
Wpierw definiujemy zmienne v1 oraz v2 i przypisujemy im wartości z odczytanego pliku xml. Zmienna v1 odpowiada za pierwszy punkt w grupie (16bit), v2 za drugi punkt (również 16 bit). Sprowadzamy te wartości do jednej zmiennej o nazwie v. W zmiennej f zostaje zapisana przekonwertowana wartość po użyciu funkcji ieee32ToFloat(). Ostatnim krokiem jest zapisanie przekonwertowanej wartości do zmiennej, która jest wyświetlana jako licznik.
Postępujemy analogicznie w przypadku odczytu mocy.
W ten sposób wizualizacja została ukończona.
Pliki do pobrania
Zasoby plików- wizualizacja
POINTS.XML
<group> <id>Miernik_n</id> <id_start>1</id_start> <len>2</len> <protocol>RTU</protocol> <dev_addr>1</dev_addr> <cmd>MB_HOLD</cmd> <address>7019</address> <access>r</access> <pool>50</pool> </group> <group> <id>Miernik_p</id> <id_start>1</id_start> <len>2</len> <protocol>RTU</protocol> <dev_addr>1</dev_addr> <cmd>MB_HOLD</cmd> <address>7023</address> <access>r</access> <pool>50</pool> </group> <group> <id>LanTick_Wej_nr_</id> <id_start>1</id_start> <len>4</len> <protocol>TCP</protocol> <dev_addr>1</dev_addr> <cmd>MB_COIL</cmd> <address>1004</address> <access>r</access> <pool>50</pool> <ip_addr>192.168.111.60</ip_addr> <ip_port>502</ip_port> </group> <group> <id>LanTick_Wyj_nr_</id> <id_start>1</id_start> <len>4</len> <protocol>TCP</protocol> <dev_addr>1</dev_addr> <cmd>MB_COIL</cmd> <address>1000</address> <access>rw</access> <pool>50</pool> <ip_addr>192.168.111.60</ip_addr> <ip_port>502</ip_port> </group> <group> <id>Temperatura_</id> <id_start>1</id_start> <len>1</len> <protocol>TCP</protocol> <dev_addr>1</dev_addr> <cmd>MB_MULTIHOLD</cmd> <address>4004</address> <access>rw</access> <pool>50</pool> <ip_addr>192.168.111.15</ip_addr> <ip_port>502</ip_port> </group>
index.htm
<html> <head> <meta http-equiv="content-type" content="text/html; charset=ISO-8859-2" > <title>Inveo</title> <link href="/style.css" rel="stylesheet" type="text/css" > <style> table{ font-family: "Trebuchet MS", sans-serif; font-size: 16px; font-weight: bold; line-height: 1.4em; font-style: normal; border-collapse:separate; width:100%; } table thead th{ padding:3px; color:#fff; text-shadow:1px 1px 1px #568F23; border:1px solid #93CE37; border-bottom:3px solid #9ED929; background-color:#9DD929; background:-webkit-gradient( linear, left bottom, left top, color-stop(0.02, rgb(123,192,67)), color-stop(0.51, rgb(139,198,66)), color-stop(0.87, rgb(158,217,41)) ); background: -moz-linear-gradient( center bottom, rgb(123,192,67) 2%, rgb(139,198,66) 51%, rgb(158,217,41) 87% ); -webkit-border-top-left-radius:5px; -webkit-border-top-right-radius:5px; -moz-border-radius:5px 5px 0px 0px; border-top-left-radius:5px; border-top-right-radius:5px; } table thead th:empty{ background:transparent; border:none; } table tbody th{ color:#fff; text-shadow:1px 1px 1px #568F23; background-color:#9DD929; border:1px solid #93CE37; border-right:3px solid #9ED929; padding:0px 3px; background:-webkit-gradient( linear, left bottom, right top, color-stop(0.02, rgb(158,217,41)), color-stop(0.51, rgb(139,198,66)), color-stop(0.87, rgb(123,192,67)) ); background: -moz-linear-gradient( left bottom, rgb(158,217,41) 2%, rgb(139,198,66) 51%, rgb(123,192,67) 87% ); -moz-border-radius:5px 0px 0px 5px; -webkit-border-top-left-radius:5px; -webkit-border-bottom-left-radius:5px; border-top-left-radius:5px; border-bottom-left-radius:5px; } table tfoot td{ color: #9CD009; font-size:32px; text-align:center; padding:3px 0px; text-shadow:1px 1px 1px #444; } table tfoot th{ color:#666; } table tbody td{ padding:3px; text-align:center; background-color:#DEF3CA; border: 2px solid #E7EFE0; -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px; color:#666; text-shadow:1px 1px 1px #fff; } </style> <script src="/ajax.js" type="text/javascript"></script> <script src="gauge.min.js"></script> <script type="text/javascript" src="segment-display.js"></script> <script type="text/javascript"> var temperature = 22.3; var voltDeviation=230.0; var power=0.0; var lantick1 = [0,0,0,0]; var lantick2_wej = [0,0,0,0]; </script> </head> <body> <div id="shadow-one"><div id="shadow-two"><div id="shadow-three"><div id="shadow-four"> <div id="page"> <div style="padding:0 0 0 5px;"><img src="logo.png" alt="Logo" style="float:left"/><span style="font-size:40px;padding-left:290;font-weight:bolder;">uSCADA</span></div> <div style="text-align:center;clear:both;width:100%;font-size:20px;font-weight:bolder;color:#555;">Embedded Modbus RTU&TCP WWW visualisation server</div> <div id="content" style="position:relative;height:530px;"> <div style="top:80;left:630; position:absolute;width:250;height:250;"> <canvas id="gauge3" width="250" height="250" data-type="canv-gauge" data-title="Power" data-min-value="0" data-max-value="1000" data-major-ticks="0 200 400 600 800 1000" data-minor-ticks="5" data-stroke-ticks="true" data-units="W" data-value-format="3.2" data-glow="true" data-animation-delay="10" data-animation-duration="200" data-animation-fn="bounce" data-colors-needle="#f00 #00f" data-highlights="0 200 #00E, 200 600 #FF1, 600 900 #0F0, 900 1000 #F00" data-onready="setInterval( function() { Gauge.Collection.get('gauge3').setValue( power );}, 1000);" ></canvas> <div style="text-align:center;width:100%">N30P Power</div> </div> <div style="top:80;left:380; position:absolute;width:250;height:250;"> <canvas id="gauge2" width="250" height="250" data-type="canv-gauge" data-title="Voltage" data-min-value="200" data-max-value="260" data-major-ticks="200 220 230 240 260" data-minor-ticks="10" data-stroke-ticks="true" data-units="V" data-value-format="3.2" data-glow="true" data-animation-delay="10" data-animation-duration="200" data-animation-fn="bounce" data-colors-needle="#f00 #00f" data-highlights="200 220 #E00, 220 240 #0F0, 240 260 #e00" data-onready="setInterval( function() { Gauge.Collection.get('gauge2').setValue( voltDeviation );}, 1000);" ></canvas> <div style="text-align:center;width:100%">N30P Voltage</div> </div> <div style="top:80;left:130; position:absolute;width:250;height:250;"> <canvas id="gauge1" width="250" height="250" data-type="canv-gauge" data-title="Temperature" data-min-value="0" data-max-value="40" data-major-ticks="0 10 20 30 40" data-minor-ticks="10" data-stroke-ticks="true" data-units="°C" data-value-format="2.1" data-glow="true" data-animation-delay="10" data-animation-duration="200" data-animation-fn="bounce" data-colors-needle="#f00 #00f" data-highlights="0 20 #33e, 20 30 #0F0, 30 40 #e33" data-onready="setInterval( function() { Gauge.Collection.get('gauge1').setValue( temperature );}, 1000);" ></canvas> <div style="text-align:center;width:100%">Nano Temperature</div> </div> <div style="width: 120px; height: 320px; position: absolute; top:50; left:890;border:2px solid #BBB;border-radius:8px;"> <h3 style="padding-left:10px"/><center>Lantick Output</center></h3> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wyj_nr_1" onClick="sendData(1, ((lantick1[0]^1)&1))"></div> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wyj_nr_2" onClick="sendData(2, ((lantick1[1]^1)&1))"></div> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wyj_nr_3" onClick="sendData(3, ((lantick1[2]^1)&1))"></div> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wyj_nr_4" onClick="sendData(4, ((lantick1[3]^1)&1))"></div> </div> <div style="width: 120px; height: 320px; position: absolute; top:50; left:0;border:2px solid #BBB;border-radius:8px;"> <h3 style="padding-left:10px"/><center>Lantick Input</center></h3> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wej_nr_1"></div> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wej_nr_2"></div> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wej_nr_3"></div> <div style="margin-left:32px;margin-bottom:5px;background:transparent url('OFFF.png');width:57px;height:57px;" id="LanTick_Wej_nr_4"></div> </div> <div id="mtbl" style="position:absolute;left:460;top:180;width:55%;"></div> <script type="text/javascript"> <!-- function updateStatus(xmlData) { setTimeout("newAJAXCommand('comm.xml', updateStatus, false)",500); if(!xmlData) return; for(var i=0;i<4;i++) { lantick1[i] = parseInt(getXMLValue(xmlData,'LanTick_Wyj_nr_'+(i+1))); var obj = document.getElementById('LanTick_Wyj_nr_'+(i+1)); if(obj) obj.style.backgroundImage = (lantick1[i]) ? 'url(/ON.png)' : 'url(/OFFF.png)'; } for(var i=0;i<4;i++) { lantick2_wej[i] = parseInt(getXMLValue(xmlData,'LanTick_Wej_nr_'+(i+1))); var obj = document.getElementById('LanTick_Wej_nr_'+(i+1)); if(obj) obj.style.backgroundImage = (lantick2_wej[i]) ? 'url(/ON.png)' : 'url(/OFFF.png)'; } var t1 = parseInt(getXMLValue(xmlData, 'Temperatura_1')); temperature = t1/10; var v1 = parseInt(getXMLValue(xmlData, 'Miernik_n1')); var v2 = parseInt(getXMLValue(xmlData, 'Miernik_n2')); var v = v2 + v1*65536; var f = ieee32ToFloat(v); voltDeviation = f.toFixed(2); v1 = parseInt(getXMLValue(xmlData, 'Miernik_p1')); v2 = parseInt(getXMLValue(xmlData, 'Miernik_p2')); v = v2 + v1*65536; f = ieee32ToFloat(v); power = f.toFixed(2); } function ieee32ToFloat(intval) { var fval = 0.0; var x;//exponent var m;//mantissa var s;//sign s = (intval & 0x80000000)?-1:1; x = ((intval >> 23) & 0xFF); m = (intval & 0x7FFFFF); switch(x) { case 0: //zero, do nothing, ignore negative zero and subnormals break; case 0xFF: if (m) fval = NaN; else if (s > 0) fval = Number.POSITIVE_INFINITY; else fval = Number.NEGATIVE_INFINITY; break; default: x -= 127; m += 0x800000; fval = s * (m / 8388608.0) * Math.pow(2, x); break; } return fval; } function sendData(id, data) { newAJAXCommand('comm.xml?data=LanTick_Wyj_nr_' + id + ';' + data); } setTimeout("newAJAXCommand('comm.xml', updateStatus, false)",2000); --> </script> </body> </html>