Serwer WWW z elementami grafiki 3D – praktyczne wykorzystanie pakietów Node.js oraz Three.js w systemach wbudowanych (część 1)
Serwer WWW – odczyt danych z procesu obsługi modułu żyroskopu
Na obecnym etapie realizacji projektu wiemy już jak nawiązać prostą komunikację pomiędzy serwerem a klientem. Do pełnej realizacji celu brakuje wciąż informacji w jaki sposób „poinformować” serwer o aktualnych danych pomiarowych, które będą zwracane przez podłączony do komputera za pomocą magistrali I2C moduł żyroskopu. Do najprostszych rozwiązań tego problemu możemy zaliczyć np. bezpośrednią implementacją obsługi żyroskopu w kodzie serwera – z wykorzystaniem operacji na plikach lub gotowych modułów Node.js, instalowanych poprzez menadżer pakietów npm.
Przykładem takiego modułu może być pakiet i2c, instalowany poleceniem:
npm install i2c
Udostępnia on proste API do realizacji niskopoziomowych operacji zapisu/odczytu danych na magistrali, np.:
var i2c = require('i2c'); var wire = new i2c(address, {device: '/dev/i2c-1'}); wire.writeByte(byte, function(err) {}); wire.writeBytes(command, [byte0, byte1], function(err) {}); wire.readByte(function(err, res) {});
Pomimo tego, że API modułu i2c jest bardzo czytelne, a ewentualna reimplementacja istniejących kawałków kodu lub bibliotek z języka C nie powinna być problematyczna, do realizacji projektu użyjemy alternatywnego podejścia. Całość obsługi modułu żyroskopu zostanie przygotowana w języku C i skompilowana do postaci pliku wykonywalnego gyro-i2c (patrz ramka poniżej). Dlaczego? Po pierwsze, większość programistów związanych z niskopoziomowymi systemami wbudowanymi nie zaryzykuje implementacji obsługi sprzętu w JavaScript (język C jest tutaj bardziej naturalnym wyborem), a po drugie – metoda ta może być przydatna, gdy nie posiadamy dostępu do kodów źródłowych aplikacji obsługujących sprzęt – wówczas jedyną możliwością jest odczyt danych ze standardowego wyjścia procesu.
<pomiarX><spacja><pomiarY><spacja><pomiarZ>
Przykładowa implementacja:
/* … */ while (1) { sleep (1); printf ("%d %d %d\n", rand()%360, rand()%360, rand()%360); fflush (stdout); } /* … */
Kompilacja:
gcc main.c -o /tmp/gyro-i2c
Do uruchomienia i komunikacji z procesem gyro-i2c z poziomu Node.js wykorzystamy wbudowany moduł child_process. Za pomocą metody spawn() utworzymy nowy proces potomny oraz zdefiniujemy dla niego funkcje zwrotną obsługi standardowego wyjścia (wywoływaną w chwili gdy program gyro-i2c zwróci kolejną porcję danych z wynikami pomiarów).
Analogicznie jak w poprzednich podpunktach, do realizacji tego etapu wykorzystamy pliki z listingu 4 oraz listingu 5. Edycję rozpoczniemy od skryptu main.js w którym zaimportujemy wbudowany moduł child_process:
var spawn = require('child_process').spawn;
W kolejnym kroku, za pomocą wywołania spawn() utworzymy nowy proces potomny realizujący kod programu gyro-i2c (skopiowany uprzednio do folderu /tmp):
var child = spawn ('/tmp/gyro-i2c');
Ostatnią modyfikacją w skrypcie main.js jest dodanie funkcji zwrotnych do obsługi kanałów stdout (funkcja przesyła odczytane dane do przeglądarki w postaci komunikatu xyz) oraz stderr (funkcja wypisuje w konsoli dane odczytane ze standardowego strumienia błędów):
child.stdout.on ('data', function (data) { io.emit ('xyz', {message: data.toString().split('\n')[0]}); }); child.stderr.on ('data', function (data) { console.log ('stderr: ' + data); });
Warto również zaimplementować obsługę zdarzenia close, która poinformuje nas o zakończeniu procesu potomnego i zwróconym przez niego kodzie wyjścia:
child.on ('close', function (code) { console.log ('exit: ' + code); });
Pełna zawartość pliku main.js została przedstawiona na listingu 6.
var http = require ('http'); var fs = require ('fs'); var spawn = require('child_process').spawn; var index = fs.readFileSync (__dirname + '/index.html'); var PORT = 8080; var server = http.createServer (function handler (request, response) { response.writeHead (200, {'Content-Type': 'text/html'}); response.end (index); }); var io = require ('socket.io').listen(server); io.on ('connection', function (socket) { console.log ('We have new connection!'); }); var child = spawn ('/tmp/gyro-i2c'); child.stdout.on ('data', function (data) { io.emit ('xyz', {message: data.toString().split('\n')[0]}); }); child.stderr.on ('data', function (data) { console.log ('stderr: ' + data); }); child.on ('close', function (code) { console.log ('exit: ' + code); }); server.listen (PORT);
Listing 6. Skrypt main.js z zaimplementowaną obsługą procesu potomnego
Przystosujmy również plik index.html do nowych wymagań projektu, tj. wyświetlenia wartości trzech pomiarów dla osi X, Y oraz Z. W tym celu w sekcji <body> utwórzmy prostą tabelę zawierającą identyfikatory pól x_val, y_val oraz z_val – listing 7.
<table> <tr> <th>X [deg]</th> <td><p id="x_val">---</p></td> </tr> <tr> <th>Y [deg]</th> <td><p id="y_val">---</p></td> </tr> <tr> <th>Z [deg]</th> <td><p id="z_val">---</p></td> </tr> </table>
Listing 7. Tabela z wynikami pomiarów żyroskopowych – plik index.html
W sekcji <head> zmodyfikujmy kod obsługi wiadomości xyz. Odczytana linia danych zostanie podzielona względem separatora ‘ ‘ (spacja), a wyniki pomiarów przypisane do poszczególnych identyfikatorów pól – listing 8.
<script> var socket = io(); socket.on ('xyz', function (data) { var arr = data.message.split(" "); document.getElementById("x_val").innerHTML = arr[0]; document.getElementById("y_val").innerHTML = arr[1]; document.getElementById("z_val").innerHTML = arr[2]; }); </script>
Listing 8. Parsowanie otrzymanych danych i przypisanie do identyfikatorów pól
Dla poprawienia estetyki utworzonej strony, w sekcji head dodajmy wpis formatujący wygląd tabeli. Pełna zawartość pliku index.html została przedstawiona na listingu 9.
<!DOCTYPE html> <html> <head> <style> table, th, td { border: 1px solid black; } th, td { border: 1px solid black; padding: 15px; } </style> <script src='/socket.io/socket.io.js'></script> <script> var socket = io(); socket.on ('xyz', function (data) { var arr = data.message.split(" "); document.getElementById("x_val").innerHTML = arr[0]; document.getElementById("y_val").innerHTML = arr[1]; document.getElementById("z_val").innerHTML = arr[2]; }); </script> </head> <body> <h1>Gyroscope I2C</h1> <table> <tr> <th>X [deg]</th> <td><p id="x_val">---</p></td> </tr> <tr> <th>Y [deg]</th> <td><p id="y_val">---</p></td> </tr> <tr> <th>Z [deg]</th> <td><p id="z_val">---</p></td> </tr> </table> </body> </html>
Listing 9. Wyświetlanie wyników pomiarów postaci tabeli – plik index.html
Po ponownym uruchomieniu serwera oraz odświeżeniu zawartości strony internetowej, generowane przez aplikację gyro-i2c, losowe wyniki pomiarów powinny zostać wyświetlone i na bieżąco aktualizowane – Rysunek 3.
Rys. 3. Prezentacja wyników pomiarów w oknie przeglądarki internetowej
Łukasz Skalski