【Websocket X IoT】Node.js上でWebsocketネットワークを構築し、ラズパイ&Arduinoをシリアル通信でデータを受信してみる
※ 当ページには【広告/PR】を含む場合があります。
2021/06/13

本ブログでは以前からWifiなどの無線機能の付いたベアメタル機器(ラズパイなど)をNode.jsで実装したMQTTサーバー&クライアントで通信のやり取りをする方法を紹介しておりました。
MQTTでIoT装置を遠隔操作する場合には、その装置がLANネットワークに繋がっていれば何処からでも指令が送れたり・受け取れたりしたわけです。
MQTTネットワークのIoTシステム構築の大前提は
ネットワークに接続できる
一方で、Arduino Unoのようにシリアル通信が主体のベアメタルが未だ長い間ロングセラーでユーザーに選ばれ続けているのは、価格の安さだけではなく、仕組みがシンプルで使いやすく、消費電力も最小に抑えられて、軽量化にも優れているなどの様々な魅力があるからだと思います。
そんな基本的にシリアル通信しかできないベアメタルをIoT化するには、一度何処かの中継機に通信接続させて、その中継機を介した遠隔操作を行うことで可能になります。

ということで今回はArduino UnoをUSB接続させたLinux機(ラズパイ)にNode.jsでWebsocketサーバーを立てつつ、内部ではWebsocketで送られた信号をシリアル信号に相互に変換してくれるアプリケーションを作成してみます。
websokect-nodeのインストール
Websocketサーバーを立てる選択肢でいうとsocket.ioも有名ですが、今回はNode.jsで軽量に動作する
node.jsが使える環境であれば概ね動作しますが、手元の環境ではnode12で動作確認しています。
以下でnpmパッケージから一発導入できます。
$ npm install websocket serialport
なおついでにこのブログ後半で使うため
Websocketサーバーのテスト
先にwebsocket-nodeを使ってWebsocketサーバーの動作確認をやっておきます。

上の図で示すように、サーバーとなるLinux機側(ここではRaspberryPi)に以下のjavascriptコードを
server.js
const webSocketsServerPort = 41337;
const webSocketServer = require('websocket').server;
const http = require('http');
const server = http.createServer((request, response) => {
// Not important for us.
// We're writing WebSocket server, not HTTP server
});
const clients = [];
server.listen(webSocketsServerPort, () => {
console.log((new Date()) + " Server is listening on port " + webSocketsServerPort);
});
const wsServer = new webSocketServer({httpServer: server});
// Event if the server recieved a request from a listed client
wsServer.on('request', (request) => {
console.log((new Date()) + ' Connection from origin ' + request.origin + '.');
const connection = request.accept(null, request.origin);
console.log((new Date()) + ' Connection accepted.');
const index = clients.push(connection) - 1;
// Event when the server recieved message from clients
connection.on('message', (message) => {
if (message.type === 'utf8') {
console.log('Received Message: ' + message.utf8Data);
} else if (message.type === 'binary') {
console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
}
});
// Event when an user disconnected
connection.on('close', (connection) => {
console.log((new Date()) + " Peer " + connection.remoteAddress + " disconnected.");
// Remove an user from the list of connected clients
clients.splice(index, 1);
});
});
ではこのコードをビルドしてみます。
$ node server.js
Wed Jun 09 2021 02:54:08 GMT+0000 (Coordinated Universal Time) Server is listening on port 41337
これでWebsocketサーバーが単独で起動しているようです。
Websocketサーバーのレスポンステスト
Websocketクライアントの実装をする前に、同じネットワーク上の別のクライアント側PCからこのWebsocketサーバー(手元の環境では192.168.0.123:41337)にアクセスしてみます。

クライアントからCurlでWebsocketサーバーのエンドポイントを叩いてレスポンスを確認するだけのやり方で試します。
$ curl -o - --http1.1 --include \
--no-buffer \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Host: 192.168.0.123:41337" \
--header "Origin: http://192.168.0.123:41337" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
http://192.168.0.123:41337/
#👇レスポンス
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: qGEgH3En71di5rrssAZTmtRTyFk=
Origin: http://192.168.0.123:41337
�
謎のバイナリが一つ返ってきましたがサーバーからのレスポンスが確認できます。
サーバー側の端末のレスポンスを覗いてみると、
$ node server.js
Wed Jun 09 2021 02:54:08 GMT+0000 (Coordinated Universal Time) Server is listening on port 41337
Wed Jun 09 2021 02:54:21 GMT+0000 (Coordinated Universal Time) Connection from origin http://192.168.0.123:41337.
Wed Jun 09 2021 02:54:21 GMT+0000 (Coordinated Universal Time) Connection accepted.
Wed Jun 09 2021 02:54:41 GMT+0000 (Coordinated Universal Time) Peer undefined disconnected.
確かにWebsocket越しにサーバー-クライアント間で通信が接続が確立されているようです。 クライアントが送信しているのが空の信号ですので、サーバ側では何も処理されず、このコネクションは20秒でタイムアウトして消滅しているようです。
ということでCurl側で出来るのは接続確認までで、きちんとした信号を送受信しようと思うとクライアント側もwebsocket-nodeで実装する必要があります。
何かしらの信号がサーバー側から送られてきたほうがWebsocketクライアントの実装がやりやすいので、以下では先にサーバーと接続されているシリアルデバイスの通信を確認していきます。
WebsocketサーバーとSerialportの連結
Websocketネットワーク間の通信は確立してそうですので、Arduinoとシリアル通信を行うための実装を追加していきます。

さて、デバイス間のシリアル通信を構築するのにもコツがいるため、本稿の内容とは別の記事でその手順を紹介してきました。
Arduinoとラズパイとのシリアル通信の構築に関しては以下の記事にまとめました。
シリアル通信とは言っても製品によって通信方式が微妙に異なるので、異なった製品を組み合わせる場合には色々と注意が必要になります。
さらに別の記事として、(最近の)node-serialportの使い方を考慮した内容も以下のリンクで解説しています。
ここで紹介しているnode-serialportの実装をそのまま流用し、上記で作りかけていた
server.js
const webSocketsServerPort = 41337;
const webSocketServer = require('websocket').server;
const http = require('http');
const server = http.createServer((request, response) => {});
const clients = [];
server.listen(webSocketsServerPort, () => {
console.log((new Date()) + " Server is listening on port " + webSocketsServerPort);
});
const wsServer = new webSocketServer({httpServer: server});
//👇シリアル通信用のライブラリ読み込み
const SerialPort = require('serialport');
const Readline = require('@serialport/parser-readline');
//👇ゲスト上(Arduino側)のSerialデバイスに名前を合せる
const portName = '/dev/ttyUSB0';
//👇接続先(Arduino)のシリアルモデムを考慮
const sp = new SerialPort(portName, {
baudRate: 9600,
dataBits: 8,
parity: 'none',
stopBits: 1,
flowControl: false,
});
//👇パーサクラスを指定
const parser = sp.pipe(new Readline());
// Event if the server recieved a request from a listed client
wsServer.on('request', (request) => {
console.log((new Date()) + ' Connection from origin ' + request.origin + '.');
const connection = request.accept(null, request.origin);
console.log((new Date()) + ' Connection accepted.');
const index = clients.push(connection) - 1;
// Event when the server recieved message from clients
connection.on('message', (message) => {
if (message.type === 'utf8') {
console.log('Received Message: ' + message.utf8Data);
//👇クライアントから受け取った文字列をデバイスへリダイレクト
sp.write(message.utf8Data, (err) => {
if (err) {
return console.log('Error: ', err.message);
}
console.log('Utf8 data written over uart.');
})
} else if (message.type === 'binary') {
console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
//👇クライアントから受け取ったバイナリをデバイスへリダイレクト
sp.write(message.binaryData, (err) => {
if (err) {
return console.log('Error: ', err.message);
}
console.log('Byte data written over uart.');
})
}
});
// Event when an user disconnected
connection.on('close', (connection) => {
console.log((new Date()) + " Peer " + connection.remoteAddress + " disconnected.");
// Remove an user from the list of connected clients
clients.splice(index, 1);
});
});
//👇ポート開放時の初期化処理
sp.on('open', ()=> {
console.log('Open Serialport');
});
//👇データ(Arduino > ラズパイ)の受信
parser.on('data', (inp_) => {
try {
console.log(`Uart Msg: ${inp_}`);
for (let i=0; i < clients.length; i++) {
//👇接続中のすべてのwsクライアントへメッセージ送信
clients[i].sendUTF(`Uart Msg: ${inp_}`);
};
} catch(e) {
return;
}
});
$ node server.js
Sun Jun 13 2021 05:26:13 GMT+0000 (Coordinated Universal Time) Server is listening on port 41337
Open Serialport
Uart Msg: LED ON
Uart Msg: LED OFF
Uart Msg: LED ON
Uart Msg: LED OFF
Uart Msg: LED ON
#...以下略
と前回の結果同様に期待通りこのWebsocketサーバーはArduinoからのシリアル信号を受け続けます。
Websocketクライアントの実装
では再びWebsocketクライアント側で利用する簡単なアプリの実装方法に話を戻します。

クライアント側の作り方も様々あると思いますが、今回はWebsocketAPIとNodejsアプリの2通りを紹介します。
①WebsocketAPIを使う
最近のモダンなブラウザであれば
ここではネットワーク上でWebsocketサーバーが
192.168.0.123:41337
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#display {
width: 500px;
height: 100px;
font-size: 2em;
}
</style>
</head>
<body>
<textarea id="display"></textarea>
<script>
const ws = new WebSocket('ws://192.168.0.248:41337');
const display = document.getElementById('display');
let currentData = {};
ws.onmessage = (event) => {display.value = `${event.data}`;}
</script>
</body>
</html>
このファイルを保存してクライアントのPCの適当なブラウザから開くと以下の動画のように動作していることが分かります。

※左側がクライアント側のブラウザ、右が稼働中のサーバープログラムからの表示。
②Nodejsネイティブで使う
Websocketクライアントで使えるライブラリもかなりありますが、ここでもサーバー・クライアント両サイドで使える
詳しい使い方は公式のドキュメントを見ていただくとして、以下のように公式のサンプルをサーバーからのメッセージを受け取るだけに修正したコードを
client.js
const WebSocketClient = require('websocket').client;
const client = new WebSocketClient();
//👇接続エラー時のイベント
client.on('connectFailed', (err) => {
console.log('Connect Error: ' + err.toString());
});
//👇接続中のイベント
client.on('connect', (connection) => {
console.log('WebSocket Client Connected...');
//👇接続時のエラー
connection.on('error', (err) => {
console.log("Connection Error: " + err.toString());
});
//👇サーバーからのメッセージ受信時
connection.on('message', (message) => {
if (message.type === 'utf8') {
console.log(`Received UTF8: ${message.utf8Data}`);
} else if (message.type === 'binary') {
console.log(`Received Binary: ${message.binaryData.length} Bytes`);
}
});
//👇切断時のイベント
connection.on('close', () => {
console.log('Client Connection Closed');
});
});
client.connect('ws://192.168.0.123:41337/');
これをネットワーク上のクライアントPCから
node client.js

※左側がクライアント側(Nodejsアプリ)、右側がWebsocketサーバーのレスポンス。
こちらもきちんと動作していることが分かります。
まとめ
以上、小規模ネットワーク上でWebsocketサーバー・クライアントを一通り構築する方法を詳しく解説してまいりましたがいかがでしたでしょうか。
対してMQTTのようにトピックという仕組みのようにデバイス間のデータを細かく仕分ける技術は無いので、特定のクライアントへ信号の送受信を行う場合には独自にサブスクリプション機能を実装しないといけないかも知れませんが、接続するデバイスの数が少ないうちはあまり考慮せずともよいかも知れません。