Wizualizacja Scada: Napięcie, Moc, Wejścia, Wyjścia oraz Temperatura

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:

Wartość jest umieszczona w dwóch kolejnych rejestrach 16 bitowych.
Definiujemy 2 punkty w grupie.

– 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&amp;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="&deg;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>