Przed paroma dniami otrzymałem maila od Sergio, który zwrócił moją uwagę na projekt WebVisu rozwijany przez Franka Benkerta w ramach SourceForge.net (https://sourceforge.net/projects/webvisu/). Autorowi udało się rozszyfrować zasady komunikacji pomiędzy sterownikiem, a standardowymi wizualizacjami webowymi opartymi o aplety Javy. Gdy już rozpracował protokół, przygotował kod, który odtwarza wizualizacje zrobione w CoDeSysie w dowolnej przeglądarce przy wykorzystaniu WYŁĄCZNIE JavaScriptu. Brzmi fascynująco? Bo jest!
Oto krótka ilustracja, o co w ogóle chodzi:
Aby zobaczyć, jak to wszystko działa należy:
Dokonania Franka Benkerta są dla mnie niesamowite z 2 powodów:
Komunikacja ze sterownikiem odbywa się przez zapytania POST wysyłane na adres /plc/webvisu.htm.
Aby odczytywać zmienne (czyli również stany wejść/wyjść) zapytanie powinno być ustrukturyzowane w następujący sposób:
| 0 | liczba-adresów-do-odczytania
| nr-zmiennej-licząc-od-zera | adres | adres | ilość-bajtów | rodzaj-zmiennej |
Odczytanie stanów 2 pierwszych wyjść na moim sterowniku odbywa się w następujący sposób:
|0|2|
0|2|48|0|0|
1|2|29|0|0|
Odpowiedź sterownika to|0|0|
Adresu, ilości bajtów i rodzaju zmiennej wcale nie trzeba znać. Dane te publikowane są przez sterownik w pliku plc_visu.xml. Piszę o tym w drugiej części artykułu.
Zmiana wartości zmiennej odbywa się komendą:
|1| liczba-adresów-do-zmiany
|nr-zmiennej-licząc-od-zera| adres | adres | ilość-bajtów | rodzaj-zmiennej | wartość |
Zmiana wartości zmiennej typu BYTE zaadresowanej przeze mnie na %MB6 na 7 wykonana zostanie komendą
|1|1|0|0|6|1|2|7|
Dla takiego amatora, jak ja, który nie lubi MODBUSA, takie zasady komunikacji są przejrzyste i proste. Do odczytania stanów 100 wyjść wystarczy prosty program w PYTHONie:
#!/usr/bin/python import requests #you might need to install requests separatelly req = "|0|100" for num in range (0,99): req+= "|"+str(num)+"|2|"+str(num)+"|1|2" req+= "|" r = requests.post('http://192.168.1.3/PLC/webvisu.htm', data=req) print r.text
Odpowiedź sterownika to:
|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|….
Czas wykonania takiego skryptu to 0.2 sek.
W javascriptcie (z jQuery) odczytanie danych wyglądałby następująco:
<script> req = "|0|100"; for (var i=0; i<100; i++) { req+="|"+i+"|2|"+i+"|1|2"; } req += "|" console.log(req); $.ajax({ type: "POST", url: "http://192.168.1.3/plc/webvisu.htm", data: req, success: function (data) { console.log(data); } }); </script>
Przewaga tego sposobu nad wykorzystywanym przeze mnie dotychczas READPI/WRITEPI jest dość oczywista. Można bardzo szybko odczytywać dużą ilość zmiennych. To duże uproszczenie wobec konieczności każdorazowego konstruowania zapytania „READPI?ADR=…..&FORMAT=%d”. Tak naprawdę powinienem teraz przepisać całą moją wtyczkę i wszystkie programy…
Poniższe 4 paragrafy są teorią dla dociekliwych. Niecierpliwi mogą od razu przewinąć do słów „Nie trzeba jednak tego wszystkiego wiedzieć”
Każda z odczytywanych lub zapisywanych zmiennych przedstawiana jest jako:
| adres | adres | ilość-bajtów | rodzaj-zmiennej |
Wygląda na to, że pierwsza część adresu to grupa. Na moim sterowniku zmienne, którym przypisałem adres (np. przez AT %MB0 : BYTE) umieszczone są w grupie 0, wejścia – 1, wyjścia – 2, inne zmienne użyte w programie – 4.
Pierwsze wyjście DO ma u mnie adres %QX3.0, jako że pierwsze trzy ‘słowa’ zajęte zostały przez moduł komunikacji RS232. 3 słowa to 3x16 bitów = 48 bitów, licząc od 0 to 0-47. Stąd też adres mojego %QX3.0 to 2,48.
Ilość bajtów zależy od rodzaju zmiennej. Dla fizycznych wejść i wyjść jest to 0 (?), dla BOOL i BYTE – 1, WORD – 2, DWORD – 4, STRING – o 1 więcej niż długość.
Rodzaje zmiennej określone są następująco: BOOL – 0, INT – 1, BYTE – 2, WORD – 3, DINT – 4, DWORD – 5, REAL – 6, TIME – 7, STRING – 8, ARRAY – 9. Choć określenie rodzaju nie jest krytyczne, tj. można odczytywać całą pamięć kawałkami 1-bajtowymi określając rodzaj jako 2. Właściwe podanie rodzaju zapewnia jednak odpowiednio sformatowaną odpowiedź. 4-znakowy string, może wrócić jako „abcd”, gdy podamy rodzaj 8, lub 47821, gdy podamy rodzaj 5.
Nie trzeba jednak tego wszystkiego wiedzieć. Kolejna rzecz, którą opisuje Frank Benkert dotyczy publikowania przez sterownik adresów zmiennych użytych w wizualizacji.
Wyobraźmy sobie prosty program z następującymi zmiennymi:
PROGRAM PLC_PRG VAR AddressedVariable AT %MB0 : BOOL; StringVariable : STRING(5); ByteVariable : BYTE; WordVariable: WORD; END_VAR
W programie takim tworzymy wizualizację „PLC_VISU” z 4 elementami wyświetlającymi wartości ww. zmiennych, oraz z piątym, który prezentuje wartość pierwszego wyjścia w sterowniku. Gdy wgramy taki program do sterownika (zakładając, że opcja Target Settings->Visualization->Web visualization jest zaznaczona), w katalogu PLC wygenerowany zostanie plik „plc_visu.xml”.
W dolnej części tego pliku znajdziemy:
<variablelist> <variable name="PLC_PRG.AddressedVariable">0,0,1,0</variable> <variable name="PLC_PRG.StringVariable">4,0,6,8</variable> <variable name="PLC_PRG.ByteVariable">4,6,1,2</variable> <variable name="PLC_PRG.WordVariable">4,7,2,3</variable> <variable name=".OUT0">2,0,0,0</variable> </variablelist>
Oto adresy, których szukaliśmy! Możemy teraz odczytać ich wartości – przy pomocy zapytań “|0|…”, READPI czy też MODBUSA. Dla mnie jest to odkrycie przełomowe. Oto mam dostęp do danych bez konieczności wcześniejszego żmudnego adresowania w sterowniku. Możemy odczytać wartość StringVariable wysyłając "|0|1|0|4|0|6|8|" lub wszystkich ww. adresów poprzez"|0|5|0|0|0|1|0|1|4|0|6|8|2|4|6|1|2|3|4|7|2|3|4|2|0|0|0|"
Zamiast więc przypisywać każdej zmiennej, którą chcemy odczytywać i zmieniać z zewnątrz, adresu poprzez „VAR variable AT %M….”, możemy umieścić na wizualizacji element, prezentujący jej wartość. Po wgraniu wizualizacji na sterownik, adres takiej zmiennej będzie do znalezienia w pliku nazwa_vizualizacji.xml.
Ważna uwaga: umieszczenie zmiennych i bloków funkcyjnych w pamięci sterownika ustalane jest przez kompilator w czasie generowania kodu programu. Każda zmiana w samym programie może prowadzić do przypisania innych adresów. Wszelkie programy integrujące powinny więc prowadzić komunikację w oparciu o adresy pozyskane każdorazowo z pliku xml… Dla programistów to jednak nic trudnego ;)
Wielkie podziękowania dla Sergio za podrzucenie mi tematu ;)