Wprowadzenie do zaawansowanego programowania stron internetowych
Dokumentacja w języku angielskim jest dostępna tutaj
W prostych przypadkach wystarczą pliki html,javascript generowane przez kreatory, o których więcej jest tutaj i tutaj. W scenariuszach bardziej zaawansowanych proponuje użyć bibliotek, który szybki start opisze w tym artykule. Kody źródłowe znajdują się tutaj
W tych przykładach pokazuję komunikację na przykładzie stron internetowych (otwartych w dwóch różnych zakładkach przeglądarki, w identyczny sposób można przesyłać dane do urządzeń typu ESP i RPi).
Prze lekturą tego artykułu koniecznie proszę się zapoznać z artykułem o ogólnie o internetowych w RemoteMe
Jak to działa
Strony są hostowane w chmurze RemoteMe maksymalny rozmiar pliku to 200KB, co powinno wystarczyć do celów, w jakim RemoteMe został stworzony, jednak gdy potrzebujesz większej przestrzeni — proszę o kontakt.
W stronach internetowych do komunikacji z systemem RemoteMe należy użyć bibliotek. Biblioteki w łatwy sposób umożliwiają komunikacje z samym systemem. Ogólnie cała komunikacja z system odbywa się przez :
- połączenie websocketowe — większość komunikacji opiera się na przesyłaniu binarnych wiadomości websocketowych w specjalnym formacie wiadomości. Format wiadomości do podglądnięcia w źródłach
- REST Api funkcje restowe dostępne są tutaj
W większości przypadków nawet do zaawansowanych scenariuszy wystarczą biblioteki javascriptowe których pełny kod źródłowy znajduję się tutaj. Znacząco ułatwiają one implementacje rozwiązań i z tych bibliotek będę korzystał w tym artykule.
( Do scenariuszy najbardziej zaawansowanych typu własny desktopowy interfejs zarządzający (czy aplikacja desktopowa, mobilna) konieczne jest implementacja wysyłania wiadomości i korzystania z REST api )
Strona internetowa od zera
zabezpieczenie
Żeby użytkownik mógł pobrać strony internetowe musi być zalogowany (jedną z trzech metod – nazwa użytkownika i hasło, token (używany np w QR kodach), pobrać specjalny token na podstawie nazwy i hasha hasła użytkownika). W innym przypadku podczas próby pobrania plików zostanie zwrócony kod błędu 401
import plików
Import bibliotek, styli — najprościej jest je po prostu skopiować z nowo utworzonej strony internetowej tworzonej z domyślnych ustawień i domyślnego szablonu. Oprócz bibliotek takich jak jquery, material design lite, dodatkowych komponentów znajdują się:
1 2 3 |
<script src="/libs/remoteMe.js"></script> <script src="/libs/remoteMeMessages.js"></script> <script src="/libs/variables.js"></script> |
- remoteMe.js – klasa RemoteMe – połączenie z RemoteMe i zapewnia wysyłanie i odbieranie wiadomości ( również wiadomości typu ping)
- remoteMeMessages.js – funkcje, klasy które umożliwiają w łatwy sposób budowanie wiadomości do wysłania i parsowanie odebranych wiadomości
- variables.js – funkcje ułatwiające komunikacje na podstawie zmiennych
1 2 |
<link rel="stylesheet" type="text/css" href="styles.css"> <script src="script.js"></script> |
Importuje pliki css i javascriptowe użytkownika te, które znajdują się po rozwinięciu belki strony internetowej — można dodawać nowe pliki javascript, css i importować je w podobny sposób.
Stałe w plikach
W plikach można korzystać ze stałych, które zostaną zamienione, gdy strona będzie serwowana do użytkwnika są to (wielkość liter nie ma znaczenia) :
- ####deviceId# – zamieniony na liczbę — deviceId strony internetowej
- ####username# – userName użytkownika
- ####raspberrypideviceid# – deviceId pierwszego znalezionego RasbperryPi – gdy w systemie nie ma RasbperryPi – 0
- ####arduinodeviceid# – deviceId pierwszego znalezionego urządzenia typu Arduino – gdy w systemie nie ma takiego urządzenia – 0
Najprostsza strona internetowa
Strona jedynie łączy się do systemu RemoteMe i utrzymuje połączenie. Dodatkowo będzie wypisywała na konsole zmiany stanu połączenia. ( w celu zachowania maksymalnej prostoty kod javascriptu jest również w kodzie html)
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<html> <head> <meta charset="utf-8"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="/libs/remoteMe.js"></script> <script src="/libs/remoteMeMessages.js"></script> <script src="/libs/remoteMeApiRest.js"></script> <script src="/libs/variables.js"></script> </head> <script> var thisDeviceId=####deviceId#; </script> <body> <button onClick="connect()">connect</button> <button onClick="disconnect()">disconnect</button> <script> function connect(){ RemoteMe.getInstance().connectWebSocket(); } function disconnect(){ RemoteMe.getInstance().disconnectWebSocket(); } function onWebSocketChange(status){ switch(status){ case ConnectingStatusEnum.CONNECTED: console.info("Connected");break; case ConnectingStatusEnum.DISCONNECTED: console.info("Disconnected");break; case ConnectingStatusEnum.FAILED: console.info("Failed");break; case ConnectingStatusEnum.CONNECTING: console.info("Connecting");break; case ConnectingStatusEnum.DISCONNECTING: console.info("Disconnecting");break; case ConnectingStatusEnum.CHECKING: console.info("Checking");break; } } new RemoteMe({automaticlyConnectWS:false,webSocketConnectionChange:[onWebSocketChange]}); </script> </body> </html> |
Skrypt ma za zadanie łączyć się z RemoteMe i rozłączać po wciśnięciu przycisków.
var thisDeviceId=####deviceId#;
id naszego urządzenia – jest używany przez biblioteki i musi zostań przypisany do zmiennej globalnej thisDeviceId
.
new RemoteMe({automaticlyConnectWS:false,webSocketConnectionChange:[onWebSocketChange]});
stworzenie obiektu klasy RemoteMe, w konfiguracji (dostępna w źródłach) informujemy, że nie chcemy łączyć się odrazu do systemu RemoteMe, oraz że za każdym razem, gdy zmieni się status połączenia chcemy wywołać funkcję onWebSocketChange:
1 2 3 4 5 6 7 8 9 10 |
function onWebSocketChange(status){ switch(status){ case ConnectingStatusEnum.CONNECTED: console.info("Connected");break; case ConnectingStatusEnum.DISCONNECTED: console.info("Disconnected");break; case ConnectingStatusEnum.FAILED: console.info("Failed");break; case ConnectingStatusEnum.CONNECTING: console.info("Connecting");break; case ConnectingStatusEnum.DISCONNECTING: console.info("Disconnecting");break; case ConnectingStatusEnum.CHECKING: console.info("Checking");break; } } |
Funkcja onWebSocketChange otrzyma jako parametr aktualny status połączenia webSocketowego. status jest typu number a ConectinStatusEnum to typ wyliczeniowy do łatwiejszego rozpoznawania stanu połączenia.
RemoteMe.getInstance()
ponieważ nie można stworzyć drugiego obiektu klasy RemoteMe najłatwiej odwołać się do remoteMe własńie przy użyciu funkcji getInstance gdy obiekt nie był wcześniej utworzony zostanie on utworzony z domyślnymi ustawieniami.
Strona w menadżerze urządzeń
- pojedyńczy plik html
- status połączenia ze stroną
Po otwarciu strony internetowej (index.html-> open in new Tab) otrzymamy:
Po kliknięciu connect strona internetowa połączy się z systemem, Status połączenia (2) zmieni się na połączony, a w logach konsoli otrzymamy stosowną informację.
Modyfikacja i odczyt stanu zmiennych
przypomnienie: zmienne -mają typ i wartość, są obserwowane przez urządzenia, zmiana na jednym urządzeniu jest propagowana do innych nasłuchujących urządzeń. Więcej o zmiennych tutaj
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 29 30 31 32 33 34 35 36 37 38 39 |
<html> <head> <meta charset="utf-8"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="/libs/remoteMe.js"></script> <script src="/libs/remoteMeMessages.js"></script> <script src="/libs/remoteMeApiRest.js"></script> <script src="/libs/variables.js"></script> </head> <script> var thisDeviceId=####deviceId#; </script> <body> <button onClick="change(1)">change to 1</button> <button onClick="change(Math.floor(Math.random() * 1000) + 1 )">change to random</button> <br/>result: <div id='resultDivId'></div> <script> function change(value){ console.info(`we will set value into ${value}`); RemoteMe.getInstance().getVariables().setInteger("someName",value); } RemoteMe.getInstance().afterWebSocketConneced(()=>{ RemoteMe.getInstance().getVariables().observeInteger("someName",value=>{ console.info(`Value was changed into ${value}`); $("#resultDivId").html(value); }); }); </script> </body> </html> |
Żeby przetestować działanie proponuje otworzyć stronę w kilku różnych zakładkach:
Zmiana wartości zmiennej na jednej zakładce powoduje zmianę wartości zmiennej an pozostałych zakładkach. Aktualny stan zmiennej pokazuje się też w zakładce “Variables” w systemie, gdzie jest też możliwość modyfikowania wartości zmiennej.
Omówienie kodu:
1 2 3 4 |
function change(value){ console.info(`we will set value into ${value}`); RemoteMe.getInstance().getVariables().setInteger("someName",value); } |
1 2 3 4 |
RemoteMe.getInstance().getVariables().observeInteger("someName",value=>{ console.info(`Value was changed into ${value}`); $("#resultDivId").html(value); }); |
W pliku: variables.js Znajdują się odpowiedniki funkcji observeInteger i setIntger dla innych typów. Oczywiście jak nasza zmienna jest zmieniana przez inne urządzenie niż strona internetowa, i inne urządzenie nasłuchuje na zmianę wartości zmiennej, postępujemy identycznie.
Wysyłanie wiadomości
O ile zmienne mają określony typ i są wysyłane do wszystkich urządzeń nasłuchujących, w niektórych przypadkach przydaje się wysłanie wiadomości o dowolnym typie do określonego urządzenia. W RemoteMe odbywa się to przez wysyłanie wiadomości. Wiadomości mają postać tablicy bajtów, w bibliotekach RemoteMe istnieją funkcje do łatwego operowania na takiej tablicy. System wspiera dwa rodzaje wiadomości:
- asynchroniczne – wysyłamy wiadomość do konkretnego urządzenia i nie oczekujemy na odpowiedź – np wiadomość zaświeć światło
- synchroniczne – wiadomości przy których czekamy na odpowiedź np – jaka jest temperatura w pokoju
( napisane przykłady jak zaświeć światło, czy odczytaj temperaturę w większości przypadków powinny być zaimplementowane przy użyciu zmiennych jednak chciałem, żeby przekaz był jasny)
Wysyłanie wiadomości asynchronicznej
Zaczniemy od wysłania wiadomości asynchronicznej. Będziemy potrzebowali dwóch urządzeń WWW, jedno o nazwie sender o dowolnym Id- i drugi o Id 1 – nazwa receiver , Ważne jest w tym pyrzpadku deviceId, bo na ten deviceId będziemy wysyłali wiadomość se strony “sender”. Oczywiście strona “receiver” po wywołaniu funkcji wysyłającej też może wiadomości wysyłać, ale w tym przypadku nie będziemy tego robić, żeby nie zaciemniać obrazu.
index.html nadajnika “sender”:
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 29 30 31 32 33 34 35 36 37 38 |
<html> <head> <meta charset="utf-8"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="/libs/remoteMe.js"></script> <script src="/libs/remoteMeMessages.js"></script> <script src="/libs/remoteMeApiRest.js"></script> <script src="/libs/variables.js"></script> </head> <script> var thisDeviceId=####deviceId#; </script> <body> <button onClick="send([1,2,3,5,8,13,21])">send fibonacci</button> <button onClick="send([2, 3, 5, 7, 11, 13, 17])">send primar</button> <button onClick="send([-123456,123456,1234567890123])">send somebig numbers</button> <script> function send(toSend){ let data= new RemoteMeData(2+toSend.length*4); data.putUint16(toSend.length); toSend.forEach(v=>{ data.putInt32(v); }); console.info(`array to sent is ${data.getArray()}`); RemoteMe.getInstance().sendUserMessage(1,data.getArray()); } RemoteMe.getInstance();//this will automaticly connect </script> </body> </html> |
Strona wysyła liczby do odbiornika (urządzenia receiver) (liczby Fibonacciego, pierwsze, i duże liczby gdzie ostatnia jest za duża do wysłania) . Gdy przesyłamy wiadomości jest ona dostarczana jako tablica bajtów, musimy zatem wiedzieć jak ją zdekodować / zakodować.
W pierwszej kolejności tworze buffor wiadomości : let data= new RemoteMeData(2+toSend.length*4);
klasa RemoteMeData
posiada metody do łatwego zapisywania zmiennych jak putShort, putString (więcej w źródłach remoteMeMessages.js ). W czasie tworzenia buforu konieczne jest podanie jego rozmiaru. rozmiar zależy od samego formatu wiadomości :
W moim przypadku w tablicy bajtów jako pierwsze dwa bajty (jako bez znakowy 16bitowy int) przesyłam ilość liczb w wiadomści: data.putUint16(toSend.length);
(2 bajty) następnie do buforu zapisuje po kolei każdą liczbę, każda liczba ma rozmiar 4 bajtów, stąd też rozmiar buforu. Następnie funkcją RemoteMe.getInstance().sendUserMessage(1,data.getArray());
wysyłam dane do urządzenia o deviceId=1
index.html urządzenia receiver (deviceId=1):
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 29 30 31 32 33 34 35 36 |
<html> <head> <meta charset="utf-8"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="/libs/remoteMe.js"></script> <script src="/libs/remoteMeMessages.js"></script> <script src="/libs/remoteMeApiRest.js"></script> <script src="/libs/variables.js"></script> </head> <script> var thisDeviceId=####deviceId#; </script> <body> <script> function onMessage(senderDeviceId,dataRaw){ let data = new RemoteMeData(dataRaw); let size=data.popUint16(); let numbers=[]; while(size--!=0){ numbers.push(data.popInt32()); } console.info(`got message from device : ${senderDeviceId} with ${numbers} numbers `); } new RemoteMe({onUserMessage:onMessage}); </script> </body> </html> |
w konstruktorze new RemoteMe({onUserMessage:onMessage});
podaję jaką funkcję biblioteka ma wywołać, gdy przyjdzie wiadomość. Funkcja odbierająca jako argument pierwszy będzie miała id urządzenia nadawczego, a jako drugi parametr tablice bajtów (jako obiekt typu DataView) w funkcji onMessage
opakowuje obiekt DataView
obiektem typu RemoteMeData
żeby było łatwiej odczytywać liczby, następnie pobieram pierwszą liczbę (2 bajtowy int bezznakowy) a następnie pobieram liczby w ilości size. Na koniec wypisuje liczby.
Oba urządzenia w menadżerze urządzeń powinny wyglądać następująco:
po uruchomieniu obu urządzeń w osobnych zakładkach w konsoli receivera będziemy mieli wypisane liczby wysłane przez urządzenie “sender”, za każdym wciśnięciem przycisku sender’a. W identyczny sposób odbieramy i wysyłamy wiadomości do innych urządzeń (ESP, RPi, aplikacja na androida etc)
Wysyłanie wiadomości synchronicznej
Czyli wysyłamy wiadomość i oczekujemy na odpowiedź – przydatne gdy chcemy “zadać pytanie naszemu ESP”. W przykładzie stworzymy dwie strony internetowe, jedna będzie pytać o sumę z dodawanie dwóch liczb, druga zrobi obliczenia i odpowie.
sender (deviceId obojętne) index.html
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 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<html> <head> <meta charset="utf-8"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="/libs/remoteMe.js"></script> <script src="/libs/remoteMeMessages.js"></script> <script src="/libs/remoteMeApiRest.js"></script> <script src="/libs/variables.js"></script> </head> <script> var thisDeviceId=####deviceId#; </script> <body> <button onClick="ask(2,2)">2+2 </button> <button onClick="ask(-12345,3456)">-12345 + 3456</button> <button onClick="ask(1.2345,3.456)">1.2345 + 3.456</button> <script> function ask(v1,v2){ let data= new RemoteMeData(16);//float used is 8bytes length data.putFloat64(v1); data.putFloat64(v2); console.info(`ask what is ${v1} + ${v2}`); RemoteMe.getInstance().sendUserSyncMessage(1,data.getArray(),(dataRaw)=>{ let data = new RemoteMeData(dataRaw); let result=data.popFloat64(); console.info(` ${v1} + ${v2} is ${result}`); alert(`${v1} + ${v2} = ${result}`) }); } RemoteMe.getInstance();//this will automaticly connect </script> </body> </html> |
Poprostu funkcją :
1 2 3 4 5 6 |
RemoteMe.getInstance().sendUserSyncMessage(1,data.getArray(),(dataRaw)=>{ let data = new RemoteMeData(dataRaw); let result=data.popFloat64(); console.info(` ${v1} + ${v2} is ${result}`); alert(`${v1} + ${v2} = ${result}`) }); |
do urządzenia o id=1 wysyłamy zapytanie. W treści wiadomości znajdują się dwie zmienny typu Float64, po otrzymaniu odpowiedzi uruchamiana jest funkcja podana jako trzeci argument, której parametrem jest odpowiedź jako tablica bajtów, która dekodujemy i wyświetlamy.
Receiver (deviceId:1)
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 29 30 31 32 33 34 35 36 37 38 39 40 |
<html> <head> <meta charset="utf-8"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="/libs/remoteMe.js"></script> <script src="/libs/remoteMeMessages.js"></script> <script src="/libs/remoteMeApiRest.js"></script> <script src="/libs/variables.js"></script> </head> <script> var thisDeviceId=####deviceId#; </script> <body> <script> function onSyncMessage(senderDeviceId,dataRaw){ let data = new RemoteMeData(dataRaw); let v1=data.popFloat64(); let v2=data.popFloat64(); console.info(`got to sum : ${v1} and ${v2} `); let toReturn= new RemoteMeData(8); toReturn.putFloat64( v1+v2); return toReturn.getArray(); } new RemoteMe({onUserSyncMessage:onSyncMessage}); </script> </body> </html> |
w konstruktorze remoteMe podajemy jaka funkcja ma zostać odpalona, gdy nadejdzie wiadomość asynchroniczna, następnie we wskazanej funkcji dekodujemy wiadomość i zwracamy tablice bajtów zawierającą jedną liczbę typu Float64. Po uruchomieniu stron w dwóch różnych zakładkach przeglądarki, po kliknięciu przycisków otrzymamy wynik z dodawania zwrócony przez stronę otwartą w drugiej zakładce.
W przyszłości umieszczę obsługę połączeń webRTC oraz streaming wideo;