tag:crieit.net,2005:https://crieit.net/tags/IoT/feed 「IoT」の記事 - Crieit Crieitでタグ「IoT」に投稿された最近の記事 2022-08-15T06:32:55+09:00 https://crieit.net/tags/IoT/feed tag:crieit.net,2005:PublicArticle/18280 2022-08-14T17:08:59+09:00 2022-08-15T06:32:55+09:00 https://crieit.net/posts/OpenBlocks-IoT-WEB OpenBlocks IoTをWEBサーバにする <p>OpenBlocks IoT をWEBサーバにする方法です。<br /> OpenBlocks IoT をセンサー類のゲートウェイとして使用し、さらにWEBサーバとすることができれば、OpenBlocks IoT 1台でセンサーからのデータを入力しモニターする等のシステムを稼働させることも可能となります。</p> <p><a href="https://crieit.now.sh/upload_images/84240a486f4bcb757b594d8970038ed062f84fdae123d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/84240a486f4bcb757b594d8970038ed062f84fdae123d.png?mw=700" alt="image" /></a></p> <p>FW4搭載の OpenBlocks IoT を対象とした解説です。FW4非搭載の OpenBlocks IoT については、汎用OSが裸で搭載されているため、それぞれのOSに応じた手段でWEBページの実装と配信が可能であり、それは難しいことではないためわざわざ解説する必要はないでしょう。</p> <blockquote> <p>FW4とは?という方はこのサイトを参照してください。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.plathome.co.jp/product/fw/fw4-application/">IoTゲートウェイソフトウェア FW4</a></p> </blockquote> <p>一方、FW4搭載の OpenBlocks IoT の場合は、FW4 が HTTP/HTTPS を使用しており、ユーザがWEBページを実装しようとする場合に「さて、どうしたものか」と悩むかもしれません。</p> <blockquote> <p>FW4 にはNode-REDが付随しており、Node-REDを用いたWEBシステム実装が可能です。ただし、以下の解説では、Node-REDを使用せずにWEBシステムを実装する方法を述べています。</p> </blockquote> <h2 id="FW4のHTTPサーバを利用する"><a href="#FW4%E3%81%AEHTTP%E3%82%B5%E3%83%BC%E3%83%90%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B">FW4のHTTPサーバを利用する</a></h2> <p>前述のとおり、FW4 は HTTP/HTTPS を使用しています。FW4 のマンマシン・インターフェースはGUIですが、これはWEBで実装されています。このため、FW4 は自身のGUIを処理するためにHTTPサーバを使用しています</p> <p><a target="_blank" rel="nofollow noopener" href="https://docs.plathome.co.jp/docs/openblocks/fw4/webui/reference/index">Debian Linux FW4 WEB-UIガイド</a></p> <p>FW4 が使用するHTTPサーバをユーザも使用することができます。FW4 のHTTPサーバに相乗りするわけです。<br /> OpenBlocks のメーカーがこれを許しているわけではありません。以下に述べる方法でWEBアプリケーションを構築する場合は自己責任でお願いします。</p> <h3 id="FW4のGUIはどこにいるのか"><a href="#FW4%E3%81%AEGUI%E3%81%AF%E3%81%A9%E3%81%93%E3%81%AB%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B">FW4のGUIはどこにいるのか</a></h3> <p>FW4 の GUI に使用されているWEBページは、ファイル・システム中のどこにいるのか、これがわかれば FW4 のHTTPサーバに容易に相乗りすることができます。<br /> これを探すのは簡単です。OpenBlocks の Debian にログインしコマンドを使用すればわかります。</p> <blockquote> <p>OpenBlocks の Debian にログインするには、FW4の設定でSSHを有効にする必要があります。<br /> <a target="_blank" rel="nofollow noopener" href="https://docs.plathome.co.jp/docs/openblocks/fw4/webui/initial/initial">Debian Linux FW4 スタートアップガイド 初期設定</a></p> </blockquote> <p>FW4 の GUI を処理しているのは PHP です。FW4 のログインのページを見ると、<code>login.php</code>となっている。<br /> <a href="https://crieit.now.sh/upload_images/5690c1081c78d2b94df4ab56361aa3ce62f8a63e5362a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/5690c1081c78d2b94df4ab56361aa3ce62f8a63e5362a.png?mw=700" alt="image" /></a><br /> <code>login.php</code> を探すと、<code>/var/webui/docroot</code>と出てくる。わかりやすいですね。</p> <pre><code class="sh">root@obsiot:~# find / -name login.php -print /var/webui/docroot/system/login.php root@obsiot:~# </code></pre> <h3 id="/var/webui/docroot"><a href="#%2Fvar%2Fwebui%2Fdocroot">/var/webui/docroot</a></h3> <p><code>/var/webui/docroot</code>はというと。</p> <pre><code class="sh">root@obsiot:~# ls -l /var/webui/docroot total 112 -rw-r--r-- 1 www-data www-data 1524 Dec 21 2021 _ctrl_datacollect.php -rw-r--r-- 1 www-data www-data 1284 Dec 20 2021 _file_del.php -rw-r--r-- 1 www-data www-data 1412 Jan 26 2022 _nodered_ctl.php -rw-r--r-- 1 www-data www-data 1076 Dec 20 2021 _ppp_con.php drwxr-xr-x 2 www-data www-data 4096 Apr 29 16:15 airmanage drwxr-xr-x 6 www-data www-data 4096 Apr 10 2020 apps drwxr-xr-x 4 www-data www-data 4096 Apr 29 16:16 css drwxr-xr-x 2 www-data www-data 4096 Apr 29 16:15 extension drwxr-xr-x 4 www-data www-data 4096 Apr 29 16:15 images -rw-r--r-- 1 www-data www-data 18740 Dec 20 2021 index.php -rw-r--r-- 1 www-data www-data 4548 Dec 21 2021 index_datacontroller.php -rw-r--r-- 1 www-data www-data 3652 Jan 26 2022 index_nodered.php drwxr-xr-x 3 www-data www-data 4096 Apr 29 16:16 js drwxr-xr-x 3 www-data www-data 4096 Apr 29 16:16 lib drwxr-xr-x 2 www-data www-data 4096 Apr 29 16:15 maintenance drwxr-xr-x 2 www-data www-data 4096 Apr 29 16:15 network -rw-r--r-- 1 www-data www-data 21 Oct 12 2020 phpinfo.php drwxr-xr-x 2 www-data www-data 4096 Apr 29 16:15 service drwxr-xr-x 2 www-data www-data 4096 Apr 29 16:16 system drwxr-xr-x 2 www-data www-data 4096 Apr 29 16:15 technical drwxrwxrwt 2 www-data www-data 4096 Apr 29 17:54 tmp -rw-r--r-- 1 www-data www-data 884 Dec 20 2021 unsupport.php root@obsiot:~# </code></pre> <p><code>phpinfo.php</code>で見てみると。<br /> <a href="https://crieit.now.sh/upload_images/af285e9fb292814cf93441e83a89e8ff62f8a8ceb8b3d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/af285e9fb292814cf93441e83a89e8ff62f8a8ceb8b3d.png?mw=700" alt="image" /></a><br /> 万全の体制です。<br /> ユーザ用のディレクトリを作成します。その下にテスト用のページを作成します。</p> <pre><code class="sh">root@obsiot:/var/webui/docroot# mkdir hoge root@obsiot:/var/webui/docroot# cd hoge root@obsiot:/var/webui/docroot# vi test.php root@obsiot:/var/webui/docroot/hoge# ls -l total 4 -rw-r--r-- 1 root root 32 Aug 14 16:48 test.php root@obsiot:/var/webui/docroot/hoge# cat test.php <?php echo "Hello, World."; ?> root@obsiot:/var/webui/docroot/hoge# </code></pre> <p><a href="https://crieit.now.sh/upload_images/876c8fb28b0bdd37893d4f47bb07be2862f969f93a855.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/876c8fb28b0bdd37893d4f47bb07be2862f969f93a855.png?mw=700" alt="image" /></a><br /> 狙いどおりです。</p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18275 2022-08-11T09:30:57+09:00 2022-08-11T22:46:08+09:00 https://crieit.net/posts/ConMas-Gateway-2 ConMas Gateway スクリプトのデバッグ (2) <p>ConMas Gateway スクリプトのテスト、デバッグをどうする、というテーマの続編です。</p> <p>前編 <a href="https://crieit.net/posts/ConMas-Gateway-1">ConMas Gateway スクリプトのデバッグ (1)</a></p> <p>結論は「スクリプト単体でテストをしましょう」です。<br /> 端末(i-Reporter)や ConMas Gateway を使うと何かと不便なので、テスト、デバッグの9割をスクリプト単体でテストをし、最後に端末と ConMas Gateway を使ったテストをしましょう、ということです。</p> <p>このような、スクリプトを例として解説を進めます。<br /> <a href="https://crieit.now.sh/upload_images/8cb3d1ab854cf71d0668d52dd6b7a8cf62f32f9c851c3.PNG" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8cb3d1ab854cf71d0668d52dd6b7a8cf62f32f9c851c3.PNG?mw=700" alt="image" /></a></p> <h2 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h2> <p>テストに必要となるデータの雛形を取得します。</p> <p>Gateway が稼働するサーバ上のディレクトリ action 下のファイルを以下のようにしておきます。</p> <blockquote> <p>Gateway は端末からのリクエストを受けると最初にこのファイルを探し出し参照します。次に Gateway は、"script"に記述されたスクリプトを起動します。</p> </blockquote> <p>ConMas/gateway/action/hogehoge.json</p> <pre><code>{ "datasource": "script", "script": "scripts/requestlog.py" } </code></pre> <p>"script"に記述するスクリプトは以下のようなスクリプトにしておきます。</p> <p>ConMas/gateway/scripts/requestlog.py</p> <pre><code class="python">import sys import json import traceback jsonData = json.loads(sys.stdin.readline()) # querydata = jsonData['query'] # postdata = jsonData['post']['clusters'] f = open('./logs/request.log', 'w') dp = json.dumps(jsonData) f.write(dp + '\n') f.close() mappings_data = [] mappings = dict(error="", mappings=mappings_data) print(json.dumps(mappings)) </code></pre> <p>この状態で、端末からGateway連携を起動させます。当然ですが、このスクリプトが実行されます。<br /> このスクリプトは、端末からのリクエストをファイルに書き込む以外に何の仕事もしません。端末は期待するデータを受け取ることができませんが、準備作業としては問題はありません。</p> <h2 id="テスト"><a href="#%E3%83%86%E3%82%B9%E3%83%88">テスト</a></h2> <p>前述のスクリプトが出力したファイルは、サーバ上のディレクトリ ConMas/gateway/logs にあります。<br /> テスト対象となる(前述の、リクエストをファイルに書く以外に何もしないスクリプトを偽物とした場合の)本物のスクリプトの入力には、端末からのリクエストの代わりに、このファイルを使用します。<br /> これでスクリプト単体でのテストが可能となります。ここからのテストには、Gateway も端末も不要です。つまり、スクリプト・エンジンがPythonであれば、Pythonスクリプトの通常のテストとまったく同様のテストが可能です。当然ですが、便利なテスト・ツールを使うことが出来ます。</p> <blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://runebook.dev/ja/docs/python/library/trace">trace-Python文の実行をトレースまたは追跡する。</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kaitolucifer/items/dc58efebd72d72a8feb2">Pythonのデバッグを完全理解</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/sky11fur/items/d4e6e2041d3bddd7b657">Pythonプログラムを追跡する</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/garaemon/items/ca72432b1890f2d793df">pythonの例外でstack traceを表示する</a></p> </blockquote> <p>忘れないうちにディレクトリ action 下のファイルを書き変えて本物にしておきましょう。<br /> ConMas/gateway/action/hogehoge.json</p> <pre><code>{ "datasource": "script", "script": "scripts/hogehoge.py" } </code></pre> <p>テスト用に取得した入力ファイルにはJSONが書かれています。ただし、見にくいのでツール等を使ってキレイにしてから使用することをおすすめします。</p> <blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://tools.m-bsys.com/development_tooles/json-beautifier.php">JSONきれい ~JSON整形ツール~</a></p> </blockquote> <p>例えば、こうやってスクリプトを実行させます。</p> <pre><code>c:\ConMas\gateway\scripts>type ..\logs\request.log | python hogehoge.py </code></pre> <p>参考までに、本物のスクリプトの姿はこんな感じです。<br /> ConMas/gateway/script/hogehoge.py</p> <pre><code class="python">import sys import json import traceback try: #----------------------------------------------------- # 帳票から送信されたデータ #----------------------------------------------------- jsonData = json.loads(sys.stdin.readline()) mappings_data = [] #----------------------------------------------------- # 帳票にレスポンスするデータを作成する処理がこの辺に書かれているはず #----------------------------------------------------- mappings = dict(error="", mappings=mappings_data) print(json.dumps(mappings)) except Exception as e: logging.debug(traceback.format_exc()) mappings = {"error": "Error: " + str(e)} print(json.dumps(mappings)) </code></pre> <p>テストにおいて入力データを変更したければ、入力ファイルの内容を書き換えれば良いのです。</p> <p>ここで紹介したテストでは、端末(帳票)とのコンビネーションをテストすることは出来ませんが、それは後ですれば良い、という考え方に基づいています。それじゃダメ、という考えの場合は始めからコンビネーションでテストしてください。</p> <p><a href="https://crieit.net/posts/ConMas-Gateway-1">ConMas Gateway スクリプトのデバッグ (1)</a></p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18272 2022-08-07T13:17:07+09:00 2022-08-07T14:17:54+09:00 https://crieit.net/posts/65978518bf6982554de02285b10e6a97 時系列データからデータの変化点を得る <p>仮想の課題と解決策を考えてみます。</p> <h2 id="課題"><a href="#%E8%AA%B2%E9%A1%8C">課題</a></h2> <p>PLCあるいはセンサーから取得されるデータの変化点(変化が発生した時刻)と、変化した際のデータの差(前のデータとの差)を取得する</p> <h3 id="取得するデータ"><a href="#%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF">取得するデータ</a></h3> <ul> <li>データに変化が生じた時刻</li> <li>前のデータとの差(対象のデータは数値で数は1つ)</li> </ul> <h3 id="条件"><a href="#%E6%9D%A1%E4%BB%B6">条件</a></h3> <ul> <li>データは1秒間隔でソースから取得され、テーブルAに記録される</li> <li>対象のデータは数値、1回に取得される数は1つ</li> <li>1回に取得されるデータは、テーブルAにレコードとして追加される</li> </ul> <h2 id="解説"><a href="#%E8%A7%A3%E8%AA%AC">解説</a></h2> <p><a href="https://crieit.now.sh/upload_images/d6a43f666a7624cd589c903e4f32b2ec62ef2e3205011.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d6a43f666a7624cd589c903e4f32b2ec62ef2e3205011.png?mw=700" alt="image" /></a></p> <blockquote> <p>さて、データ・ソースから取得されたデータが無秩序にデータベース中のテーブルに書き込まれます。これはありがちなケースです。PLCやセンサーからデータを取得するゲートウェイの設計担当者が、出来る努力をしなかったケースですね。(もちろん、全てのデータがほしい、というオーダーがあった可能性は否定できませんが)<br /> このようなお粗末を許すプロジェクトですから、他の失敗も考えられます。<br /> 簡単に想像できる失敗は、目的とするデータを計算するバッチ・プログラムを作り出すことです。このプログラムはスケジューラで定期的に実行されます。良さそうな設計ですが、はたしてこのプログラムの処理はデータの発生に追いつくのでしょうか。もし追いつかない場合には、対症療法的な対策が取られるでしょう。それは、チューニングと称して行われる、データベースに対する様々な工夫です。それでも解決できない場合には、ハードウェアの設計担当者のミスが指摘されるかもしれません。こうしてプロジェクトは「計画になかった役に立たない不要な作業」のために大きく複雑になっていきます。<br /> 想像したくもない状況ですが、情報システム開発のプロジェクトでは、今でもこれに類する失敗が繰り返されています。</p> </blockquote> <p>経験からではなく歴史(学問)から学ぶ賢者はこう考えます「データへの処理は、そのデータが発生したときにしろ」。<br /> データ・ドリブン型システムとは、本来はこのようなことを指し、大昔からソフトウェアの工学分野で有効性が唱えられていました。ただし、今回のケースでは、データが発生した時には何も手出しができません。条件から読み取ると、発生したデータはもれなくテーブルAに書き込まれ、テーブルAに書き込まれる前になにかの工夫はできなさそうです。<br /> となると、可能で有効な細工は、データがテーブルAに追加される部分にすべきのようです。そして、もう一歩考えを進めると、データベースの基礎的な知識さえあれば、使うべきはトリガーだということに簡単に気づくことができます。<br /> 後述の解決策の一例でもトリガーを活用しています。</p> <h2 id="解決策の一例"><a href="#%E8%A7%A3%E6%B1%BA%E7%AD%96%E3%81%AE%E4%B8%80%E4%BE%8B">解決策の一例</a></h2> <h3 id="テーブルA"><a href="#%E3%83%86%E3%83%BC%E3%83%96%E3%83%ABA">テーブルA</a></h3> <p>解決策を具体的に説明するために、テーブルAを以下のように仮定します。これは、かつての僕の記事で何度か登場した"from_plc"ですね。</p> <pre><code>mysql> desc from_plc; +-------------+-----------+------+-----+-------------------+-------------------+ | Field | Type | Null | Key | Default | Extra | +-------------+-----------+------+-----+-------------------+-------------------+ | body | json | YES | | NULL | | | time_insert | timestamp | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | +-------------+-----------+------+-----+-------------------+-------------------+ 2 rows in set (0.02 sec) mysql> </code></pre> <p>見てのとおり、モデルはMySQLで実装されています。</p> <h3 id="トリガー"><a href="#%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC">トリガー</a></h3> <p>計算と目的のデータをピックアップするために、トリガーを使います。トリガーは以下のとおりです。</p> <pre><code>DROP TRIGGER IF EXISTS from_plc_proc; DELIMITER // CREATE TRIGGER from_plc_proc AFTER INSERT ON from_plc FOR EACH ROW BEGIN DECLARE count_new INTEGER; DECLARE count_old INTEGER; DECLARE count_rec INTEGER; -- from_plc_last の参照 SELECT COUNT(*) INTO count_rec FROM from_plc_last; IF count_rec > 0 THEN SELECT JSON_EXTRACT(body, '$.value') INTO count_old FROM from_plc_last; ELSE INSERT INTO from_plc_last (time_insert) VALUES (NOW()); SET count_old = 0; END IF; -- from_plc_last の更新 UPDATE from_plc_last SET body = NEW.body, time_insert = NEW.time_insert; SET count_new = JSON_EXTRACT(NEW.body, '$.value'); -- count_status の更新 IF count_new <> count_old THEN INSERT INTO count_status (count_old, count_new, time_change) VALUES (count_old, count_new, NEW.time_insert); END IF; END; // DELIMITER ; </code></pre> <p>3つのテーブルが登場します。</p> <ul> <li>from_plc : 前述のテーブルAの実態</li> <li>from_plc_last : from_plc中の最新のレコードのコピーが置かれる</li> <li>count_status : 目的のデータの導出が可能なデータが記録される</li> </ul> <p>各テーブルの属性は以下のとおりです。from_plcは既出なので、あらためて示すことはしません。</p> <pre><code>mysql> desc from_plc_last; +-------------+-----------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+-----------+------+-----+---------+-------+ | body | json | YES | | NULL | | | time_insert | timestamp | YES | | NULL | | +-------------+-----------+------+-----+---------+-------+ 2 rows in set (0.01 sec) mysql> desc count_status; +-------------+-----------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------------+-----------+------+-----+---------+-------+ | count_old | int | YES | | NULL | | | count_new | int | YES | | NULL | | | time_change | timestamp | YES | | NULL | | +-------------+-----------+------+-----+---------+-------+ 3 rows in set (0.01 sec) mysql> </code></pre> <p>このシステムでは、from_plc_last はヒット率100%のキャッシュとして機能し、前後のデータの差を計算する処理の効率化に寄与します。</p> <h2 id="テスト"><a href="#%E3%83%86%E3%82%B9%E3%83%88">テスト</a></h2> <p>上記の解決策をテストしてみました。</p> <h3 id="データ"><a href="#%E3%83%87%E3%83%BC%E3%82%BF">データ</a></h3> <p>以前の僕の記事「サーバやPC上のプログラムでModbus機器からデータを取得 (2)」で登場したPerlスクリプトの劣化版とModbusシミュレーターでデータを発生させました。</p> <p><a href="https://crieit.net/posts/PC-Modbus-2">サーバやPC上のプログラムでModbus機器からデータを取得 (2)</a></p> <p>Perlスクリプトの46, 68行目をコメント・アウトしただけです。</p> <h3 id="結果"><a href="#%E7%B5%90%E6%9E%9C">結果</a></h3> <p>from_plc</p> <pre><code>mysql> select * from from_plc; +----------------------------------------------------+---------------------+ | body | time_insert | +----------------------------------------------------+---------------------+ | {"ts": "2022-08-07T12:31:52.445466", "value": 0} | 2022-08-07 12:31:52 | | {"ts": "2022-08-07T12:31:53.492522", "value": 0} | 2022-08-07 12:31:53 | | {"ts": "2022-08-07T12:31:54.547653", "value": 0} | 2022-08-07 12:31:54 | | {"ts": "2022-08-07T12:31:55.563391", "value": 0} | 2022-08-07 12:31:55 | | {"ts": "2022-08-07T12:31:56.610040", "value": 0} | 2022-08-07 12:31:56 | | {"ts": "2022-08-07T12:31:57.672496", "value": 1} | 2022-08-07 12:31:57 | | {"ts": "2022-08-07T12:31:58.703830", "value": 1} | 2022-08-07 12:31:58 | | {"ts": "2022-08-07T12:31:59.734885", "value": 1} | 2022-08-07 12:31:59 | | {"ts": "2022-08-07T12:32:00.766081", "value": 1} | 2022-08-07 12:32:00 | | {"ts": "2022-08-07T12:32:01.797569", "value": 1} | 2022-08-07 12:32:01 | | {"ts": "2022-08-07T12:32:02.859724", "value": 2} | 2022-08-07 12:32:02 | | {"ts": "2022-08-07T12:32:03.891062", "value": 2} | 2022-08-07 12:32:03 | | {"ts": "2022-08-07T12:32:04.958819", "value": 2} | 2022-08-07 12:32:04 | | {"ts": "2022-08-07T12:32:06.5521", "value": 2} | 2022-08-07 12:32:06 | +----------------------------------------------------+---------------------+ 14 rows in set (0.01 sec) mysql> </code></pre> <p>from_plc_last</p> <pre><code>mysql> select * from from_plc_last; +------------------------------------------------+---------------------+ | body | time_insert | +------------------------------------------------+---------------------+ | {"ts": "2022-08-07T12:32:06.5521", "value": 2} | 2022-08-07 12:32:06 | +------------------------------------------------+---------------------+ 1 row in set (0.00 sec) mysql> </code></pre> <p>count_status</p> <pre><code>mysql> select * from count_status; +-----------+-----------+---------------------+ | count_old | count_new | time_change | +-----------+-----------+---------------------+ | 0 | 1 | 2022-08-07 12:31:57 | | 1 | 2 | 2022-08-07 12:32:02 | +-----------+-----------+---------------------+ 2 rows in set (0.00 sec) mysql> </code></pre> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18271 2022-08-06T19:02:04+09:00 2022-08-11T09:31:57+09:00 https://crieit.net/posts/ConMas-Gateway-1 ConMas Gateway スクリプトのデバッグ (1) <p>製造現場の紙帳票のデジタル化を担うi-Reporter。現段階ではまだベストなソリューションとは言えませんが、今後の機能アップが期待出来るとすればそのポテンシャルは大きく、紙帳票のデジタル化に留まらず、旧態依然な現場のパネルまでも、i-ReporterをのせたWindowsのタッチパネルに入れ替わるかもしれません。日本の製造業をチャンピオンに返り咲かせるDXの誘因として重要なソリューションのひとつであることに間違いはありません。。<br /> このi-Reporterシステムに必須といってもよいのが"ConMas Gateway"です。まだ機能面での不足がある ConMas Gateway ですが、i-Reporter の jQuery.ajax とも言える ConMas Gateway を使わずして i-Reporter の存在意義は語れません。</p> <p>この記事では、ConMas Gateway スクリプトの効率的なデバッグ手法について複数回に分けて解説します。</p> <blockquote> <p>この記事の内容は、IT系開発に不慣れなFA系システム開発者向けです。WEBシステムのサーバ・サイド開発等に長けたITエンジニアにとっては、当たり前のことが書かれています。</p> <p>この記事の内容を理解するために、i-Reporterに関する細かな知識を事前に得ている必要はありません。ただし、この知識がない方は、ある程度の想像力を働かせながら読み進める必要があります。</p> </blockquote> <h2 id="システム・モデル"><a href="#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%83%BB%E3%83%A2%E3%83%87%E3%83%AB">システム・モデル</a></h2> <p><a href="https://crieit.now.sh/upload_images/62f914a7bc277c864df20629b59fa44862ee2e6f8df37.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/62f914a7bc277c864df20629b59fa44862ee2e6f8df37.png?mw=700" alt="image" /></a></p> <ul> <li>ConMas Gateway から起動されたスクリプトは、i-Reporter からPOST(あるいはGET)されたデータを、標準入力を介して ConMas Gateway から受け取ります。</li> <li>スクリプトは i-Reporter へのレスポンスを標準出力を介して ConMas Gateway へ渡し、ConMas Gateway はこれを i-Reporter にレスポンスします。</li> </ul> <p>つまり、ConMas Gateway のスクリプトは、WEBシステムにおける CGIを使うスクリプトと何ら変わりがありません。</p> <blockquote> <p>面白いのは、ConMas Gateway と ConMas Server の間にインタフェースが全く存在せず、両者は何のやりとりもしないことです。<br /> もちろん、ConMas Server のデータベースは公開されているため、ConMas Gateway が直接あるいはスクリプトを介して、このデータベースをアクセスすることは可能です。</p> </blockquote> <p>なお、ConMas Gateway はスクリプトがなくても、自らの機能でデータベースをアクセスし結果を i-Reporter にレスポンスすることが出来ます。ただし、このデータベースのアクセスはシンプルなものに限定され、複雑な処理、あるいはデータベース・アクセス以外の処理を行うにはスクリプトが必要となります。<br /> この記事では、このスクリプトのデバッグについて述べており、ConMas Gateway 自身が持つデータベース・アクセスについては述べていません。</p> <h2 id="データ"><a href="#%E3%83%87%E3%83%BC%E3%82%BF">データ</a></h2> <p>i-Reporter とスクリプトの間でやりとりされるデータはJSONです。これは、データを処理する手続きをシンプルに記述できるという点で歓迎できます。</p> <ul> <li>POSTされるデータは、クラスタ(i-Reporterで処理される帳票上のデータの最小単位)の属性と(当該クラスタに入力された)データが key: value のタプルとなったJSONです。複数のタプルが配列で並びます。どのクラスタをPOSTするかは、事前に ConMas Designer 上で選択します。</li> <li>GETされるデータは任意です。ConMas Designer による設定でURLに続くデータを任意に記載することが出来ます。</li> <li>レスポンスは、帳票上のクラスタのIDと当該クラスタに表示されるデータをタプルにしたJSONです。もちろん、このタプルは配列で複数にすることができます。</li> </ul> <blockquote> <p>この記事では、データのフォーマットに関する細かな解説をしません。データのフォーマットに関する細かなルールや、ConMas Designer での設定については、シムトップス社から提供されるマニュアルを参照してください。これらの情報がなくても、この記事を読み進める上での支障にはなりません。<br /> マニュアルにアクセスするには、シムトップス社のサイトにアクセスするためのIDが必要になります。</p> </blockquote> <h2 id="標準と思われるデバッグ手法"><a href="#%E6%A8%99%E6%BA%96%E3%81%A8%E6%80%9D%E3%82%8F%E3%82%8C%E3%82%8B%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E6%89%8B%E6%B3%95">標準と思われるデバッグ手法</a></h2> <p>標準と思われるデバッグの手法は、i-Reporterで(必要であれば帳票にデータを入力し)帳票上のアクション・クラスタを発動させ、当該クラスタに設定されたスクリプトを動作させながらテストと不具合の解消を進めるという(何の工夫もない)いたって普通の方法です。<br /> 小さく単純でテストが簡単なスクリプトのデバッグでは、この方法でも良いかもしれません。ただし、大きく複雑で、複雑、高度なテストとデバッグを要するスクリプトの場合には問題が生じます。</p> <h4 id="問題"><a href="#%E5%95%8F%E9%A1%8C">問題</a></h4> <ul> <li>この方法ではスクリプトの動作を簡単にはトレースすることができない</li> <li>この方法でのスクリプトのトレースは、デバッグのための古典的なプリント、あるいはカバレージをファイル等に出力するなどが必要となる。一方で、Python で提供される Pdb や Trace 等のコマンドラインで容易に使えるツールが使用できない。</li> </ul> <h2 id="次回"><a href="#%E6%AC%A1%E5%9B%9E">次回</a></h2> <p>続きは次回とさせていただきます。<br /> 初心者の方々のために、ゆっくりとマッタリと進めさせていただきます。<br /> 次回は「ではどうすれば良いか」です。</p> <p><a href="https://crieit.net/posts/ConMas-Gateway-2">ConMas Gateway スクリプトのデバッグ (2)</a></p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18267 2022-07-30T15:43:22+09:00 2022-07-30T15:57:47+09:00 https://crieit.net/posts/PC-Modbus-2 サーバやPC上のプログラムでModbus機器からデータを取得 (2) <p>サーバやPC上のプログラムでModbus機器からデータを取得、その第二回です。<br /> 第一回では、Android上のModbusシミュレータからデータを取得するための、Perlスクリプトのサンプルを紹介しました。Perlでは問題があるという方は、他の言語で記述しても原理は変わらないはずなので、他の言語で実装してみてください。</p> <p><a href="https://crieit.net/posts/PC-Modbus-1">サーバやPC上のプログラムでModbus機器からデータを取得 (1)</a></p> <p>今回は、取得したデータをデータベースに保存します。</p> <h2 id="データベース"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9">データベース</a></h2> <p>以前の記事で使用した、MySQLとテーブル"from_plc"<br /> を使います。</p> <p><a href="https://crieit.net/posts/PLC-JSON-4">PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (4)</a></p> <pre><code>C:\Users\hoge\Downloads>mysql -u hoge -p Enter password: ******* Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 60 Server version: 8.0.29 MySQL Community Server - GPL Copyright (c) 2000, 2022, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> use hoge Database changed mysql> desc from_plc; +-------------+-----------+------+-----+-------------------+-------------------+ | Field | Type | Null | Key | Default | Extra | +-------------+-----------+------+-----+-------------------+-------------------+ | body | json | YES | | NULL | | | time_insert | timestamp | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | +-------------+-----------+------+-----+-------------------+-------------------+ 2 rows in set (0.02 sec) mysql> </code></pre> <h2 id="Perlスクリプト"><a href="#Perl%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88">Perlスクリプト</a></h2> <p>前回は、Perlスクリプトを僕のセルフォンTermux上で動作させましたが、今回はPCのWindows上で開発し動作させます。何故?</p> <blockquote> <p>僕のセルフォンのTurmuxのcpanになんらかの問題があるらしく、DBIのビルドができないんです。いろいろトライしたけど僕には解決は無理と判断し、Turmax上のPerlスクリプトを放棄しました。<br /> DBIは必須ではないですが、データベースをアクセスするPerlなら、DBIを使うのが一般的ですよね。<br /> DBIのない環境で、これ以上Perlを書く気にはなれませんよね。まあ、僕のせいなんですけどね。</p> </blockquote> <p>「サーバやPC上のプログラムでModbus機器からデータを取得 (1)」で作成したスクリプトに手を加えました。これで、Modbus機器からModbus/TCPにより取得したデータをデータベースに保存します。</p> <pre><code class="perl">#!/usr/bin/perl use IO::Socket; use DBI; use Time::HiRes "gettimeofday"; use utf8; use Encode; use JSON; { my $Server = 'localhost'; my $Port = 502; my $TransactionID = 0x0200; my $ProtocolID = 0x0000; my $Length = 0x0006; my $UnitID = 0x01; my $FunctionCode = 0x03; my $StartAddress = 0x0000; my $CountRegister = 0x0001; my $req = pack("n3C2n2", $TransactionID, $ProtocolID, $Length, $UnitID, $FunctionCode, $StartAddress, $CountRegister); my $old = -1; while (1) { my $socket = new IO::Socket::INET( PeerAddr=>$Server, PeerPort=>$Port, Proto=>'tcp'); die "IO::Socket : $!" unless $socket; my $size = $socket->send($req); shutdown($socket, 1); my $response = ""; $socket->recv($response, 1024); my @data = unpack("n3C3n", $response); my $Register = $data[6]; if ($old != $Register) { print "sent data:\n"; BinaryDump($req); print "received response:\n"; BinaryDump($response); my $now = getCurrentTimeStr(); my $output_data = { 'ts' => $now, 'value' => $Register }; my $json_text = decode('utf-8', encode_json( $output_data )); print $json_text . "\n"; my @values = ($json_text); $user = 'hoge'; $passwd = 'hoge001'; $db = DBI->connect('DBI:mysql:hoge:localhost', $user, $passwd); $sth = $db->prepare("INSERT INTO from_plc (body) values (?)"); $sth->execute(@values); $sth->finish; $db->disconnect; } $old = $Register; $socket->close(); sleep 1; } } # 利用させていただきました # https://netlog.jpn.org/r271-635/2018/11/perl-bynary-dumper.html sub BinaryDump { my ($buf) = @_; my $len; my $i; $len = length($buf); printf ("length = %d\n", $len); for ($i = 0; $i < $len; $i++) { printf("%02X ", ord(substr($buf, $i, 1))); # 16文字目で画面上の改行 if (($i % 16) == 15) { print "\n"; } } if (($i % 16) != 15) { print "\n"; } } # 利用させていただきました # https://akrad.hatenablog.com/entry/2018/10/20/234528 sub getCurrentTimeStr { my ($epochSec, $microSec) = gettimeofday(); my ($sec, $min, $hour, $day, $mon, $year) = localtime($epochSec); $year += 1900; $mon++; return "$year" . '-' . sprintf("%02d", $mon) . '-' . sprintf("%02d", $day) . 'T' . sprintf("%02d", $hour) . ':' . sprintf("%02d", $min) . ':' . sprintf("%02d", $sec) . '.' . "$microSec"; } </code></pre> <p>Perlの自由さと気楽さが良いですね。</p> <h2 id="テスト"><a href="#%E3%83%86%E3%82%B9%E3%83%88">テスト</a></h2> <p>Windows上ということで、再び MOD-RSsim に登場いただきます。MOD-RSsimを起動しておきます。<br /> <a href="https://crieit.now.sh/upload_images/ce54d89db6542b8d42e049dac808c83862e4cf06c02a0.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ce54d89db6542b8d42e049dac808c83862e4cf06c02a0.png?mw=700" alt="image" /></a><br /> Perlスクリプトを実行します。</p> <pre><code>C:\Users\hoge\Downloads>perl sample.pl sent data: length = 12 02 00 00 00 00 06 01 03 00 00 00 01 received response: length = 11 02 00 00 00 00 05 01 03 02 00 00 {"ts":"2022-07-30T15:05:59.916895","value":0} </code></pre> <p>Holding Register 40001 の0を取得しデータベースに保存されます。</p> <p>MOD-RSsim で 40001 に任意の数値を入力します。<br /> <a href="https://crieit.now.sh/upload_images/365c981a39ba1de3af4b05aa11b1bcf162e4cfae8fe0a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/365c981a39ba1de3af4b05aa11b1bcf162e4cfae8fe0a.png?mw=700" alt="image" /></a></p> <p>40001 のデータが変わる都度、データベースに値が保存されます。</p> <pre><code>C:\Users\hoge\Downloads>perl sample.pl sent data: length = 12 02 00 00 00 00 06 01 03 00 00 00 01 received response: length = 11 02 00 00 00 00 05 01 03 02 00 00 {"ts":"2022-07-30T15:05:59.916895","value":0} sent data: length = 12 02 00 00 00 00 06 01 03 00 00 00 01 received response: length = 11 02 00 00 00 00 05 01 03 02 00 01 {"ts":"2022-07-30T15:06:04.10446","value":1} sent data: length = 12 02 00 00 00 00 06 01 03 00 00 00 01 received response: length = 11 02 00 00 00 00 05 01 03 02 00 02 {"ts":"2022-07-30T15:06:07.41537","value":2} sent data: length = 12 02 00 00 00 00 06 01 03 00 00 00 01 received response: length = 11 02 00 00 00 00 05 01 03 02 00 03 {"value":3,"ts":"2022-07-30T15:06:10.72655"} sent data: length = 12 02 00 00 00 00 06 01 03 00 00 00 01 received response: length = 11 02 00 00 00 00 05 01 03 02 00 04 {"value":4,"ts":"2022-07-30T15:06:12.150828"} sent data: length = 12 02 00 00 00 00 06 01 03 00 00 00 01 received response: length = 11 02 00 00 00 00 05 01 03 02 00 05 {"ts":"2022-07-30T15:06:14.213053","value":5} Terminating on signal SIGINT(2) C:\Users\hoge\Downloads> </code></pre> <p>データベースのテーブル"from_plc"を確認してみます。</p> <pre><code>C:\Users\hoge\Downloads>mysql -u hoge -p Enter password: ******* Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 75 Server version: 8.0.29 MySQL Community Server - GPL Copyright (c) 2000, 2022, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> use hoge Database changed mysql> select * from from_plc; +----------------------------------------------------+---------------------+ | body | time_insert | +----------------------------------------------------+---------------------+ | {"ts": "2022-07-30T15:05:59.916895", "value": 0} | 2022-07-30 15:05:59 | | {"ts": "2022-07-30T15:06:04.10446", "value": 1} | 2022-07-30 15:06:04 | | {"ts": "2022-07-30T15:06:07.41537", "value": 2} | 2022-07-30 15:06:07 | | {"ts": "2022-07-30T15:06:10.72655", "value": 3} | 2022-07-30 15:06:10 | | {"ts": "2022-07-30T15:06:12.150828", "value": 4} | 2022-07-30 15:06:12 | | {"ts": "2022-07-30T15:06:14.213053", "value": 5} | 2022-07-30 15:06:14 | +----------------------------------------------------+---------------------+ 6 rows in set (0.00 sec) mysql> </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>このように、Modbus機器からのデータ取得は、専用の機器や商用のソフトウェアがなくても可能です。今回はModbus/TCPでしたが、RTUでも原理は同じです。<br /> データ取得だけではなく、Modbus機器にデータを書き込むことも難しくはありません。Modbusの仕様を理解すれば、データの取得も書き込みも簡単です。<br /> Modbusに限らず、各メーカのPLCの仕様もほとんどがオープンになっているので、それぞれの仕様を理解しさえすれば、Modbusと同じように自分で書いたプログラムでの読み書きが可能です。<br /> 様々な種類のPLCやリモートIOなどをアクセスする場合には、ゲートウェイなどを利用したほうが良いかもしれません。一方で、限られた種類のPLCやリモートIOをアクセスする場合は、自分で書いたプログラムを使用するという選択もあると思います。</p> <p>「作らない」のではなく「作れない」んです。なぜ「作れない」のか?「作らない」からです。</p> <p><a href="https://crieit.net/posts/PC-Modbus-1">サーバやPC上のプログラムでModbus機器からデータを取得 (1)</a></p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18262 2022-07-26T17:14:42+09:00 2022-07-30T18:42:44+09:00 https://crieit.net/posts/PC-Modbus-1 サーバやPC上のプログラムでModbus機器からデータを取得 (1) <p>サーバやPC上のプログラムでModbus機器からデータを取得、なんて場面があります。例えば、接点やセンサーがつながったリモートIOからデータを取得したい場合などです。</p> <p>この記事では、Modbus/TCPでリモートIOなどからデータを取得するサンプル・プログラムを紹介します。</p> <p>この記事で紹介するサンプルはModbus/TCPを前提としていますが、Modbus/RTUでも原理は同じです<br /> 。Socketがシリアル・インタフェースに変わり、やりとりするデータのフォーマットがTCPからRTUに変わるだけです。<br /> ※RTUの場合、送受するデータにCRCを組み込む必要があり、少々やっかいですけどね。</p> <blockquote> <p>リモートIOのメーカーのサイトでは、PLCがリモートIOからデータを取得するように書かれていたりします。リモートIOからのデータを使って機械を制御する場合はそんな構成なのかもしれませんが、そのデータをサーバやPCで処理したい場合には、リモートIOのデータをサーバやPCが直接取得するのが合理的です。リモートIOとPLCは必ずしもセットではないのです。<br /> 「そんなことは、ここであらためて書かなくてもあたりまえだろ」と思われる方も多いでしょうが、固定観念と先入観が、迷わず(本来は不要であるはずの)PLCを登場させてしまいます。</p> </blockquote> <h2 id="Modbus/TCP"><a href="#Modbus%2FTCP">Modbus/TCP</a></h2> <p>Modbusについては、エム・システム技研さまのこのドキュメントがおすすめです。</p> <p><a target="_blank" rel="nofollow noopener" href="http://www.m-system.co.jp/mssjapanese/kaisetsu/nmmodbus.pdf">Modbus プロトコル概説書</a></p> <h2 id="サンプル・プログラム"><a href="#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%83%BB%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0">サンプル・プログラム</a></h2> <p>今回はPerlを使いました。Perlである必要性はありませんが、Perlではだめだという理由もありません。なぜPerlを選択したか?僕のセルフォンでPerlが使えたからです。</p> <p>サンプルでは、ファンクション0x03: Read Holding Register を使っています。</p> <p>データをコンソールに16進表記で表示するために、公開されていたコレを遠慮なく拝借いたしました。<br /> <a target="_blank" rel="nofollow noopener" href="https://netlog.jpn.org/r271-635/2018/11/perl-bynary-dumper.html">(Perl) バイナリ変数の16進ダンプ表示</a></p> <pre><code class="perl">#!/usr/bin/perl use IO::Socket; { my $Server = 'localhost'; my $Port = 5502; my $socket = new IO::Socket::INET( PeerAddr=>$Server, PeerPort=>$Port, Proto=>'tcp'); die "IO::Socket : $!" unless $socket; print "connected to the server\n"; my $TransactionID = 0x0200; my $ProtocolID = 0x0000; my $Length = 0x0006; my $UnitID = 0x01; my $FunctionCode = 0x03; my $StartAddress = 0x0000; my $CountRegister = 0x0001; my $req = pack("n3C2n2", $TransactionID, $ProtocolID, $Length, $UnitID, $FunctionCode, $StartAddress, $CountRegister); my $size = $socket->send($req); print "sent data:\n"; BinaryDump($req); shutdown($socket, 1); my $response = ""; $socket->recv($response, 1024); print "received response:\n"; BinaryDump($response); $socket->close(); } # 利用させていただきました # https://netlog.jpn.org/r271-635/2018/11/perl-bynary-dumper.html sub BinaryDump { my ($buf) = @_; my $len; my $i; $len = length($buf); printf ("length = %d\n", $len); for ($i = 0; $i < $len; $i++) { printf("%02X ", ord(substr($buf, $i, 1))); # 16文字目で画面上の改行 if (($i % 16) == 15) { print "\n"; } } if (($i % 16) != 15) { print "\n"; } } </code></pre> <h2 id="テスト"><a href="#%E3%83%86%E3%82%B9%E3%83%88">テスト</a></h2> <p>テストにも僕のセルフォンを使いました。<br /> Modbusスレーブとして、Androidアプリケーション"ModTCPSimX"に活躍いただきました。<br /> <a href="https://crieit.now.sh/upload_images/672850389bd205ccd2287edea4b6eea462df857ac0e06.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/672850389bd205ccd2287edea4b6eea462df857ac0e06.png?mw=700" alt="image" /></a><br /> ModTCPSimX のポート番号のデフォルトは5502です。僕のプログラム側も5502にしてあります。<br /> ※一般的なModbus/TCPは502です。<br /> Holding Register 40001 に任意の数値を設定しておきます。<br /> ※ModTCPSimX では値の背景が黄色の場合は更新がコミットされていません。鉛筆アイコンでコミットするのを忘れずに<br /> <a href="https://crieit.now.sh/upload_images/e0e934f699c14d7e14657cced9848d5c62df8d8718344.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e0e934f699c14d7e14657cced9848d5c62df8d8718344.png?mw=700" alt="image" /></a><br /> Termuxでプログラムを動かすと、最後のバイトに値が置かれたTXが返ります。<br /> <a href="https://crieit.now.sh/upload_images/a486c6b8599b14f88005f174460cdbd662df8dad794c7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a486c6b8599b14f88005f174460cdbd662df8dad794c7.png?mw=700" alt="image" /></a></p> <h2 id="次回"><a href="#%E6%AC%A1%E5%9B%9E">次回</a></h2> <p>せっかくなので、次回は取得したデータをデータベースに保存します。どうってことないんですけどね。</p> <p><a href="https://crieit.net/posts/PC-Modbus-2">サーバやPC上のプログラムでModbus機器からデータを取得 (2)</a></p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18256 2022-07-23T23:14:58+09:00 2022-07-23T23:47:21+09:00 https://crieit.net/posts/PLC-JSON-6 PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (6) <p>PLCからゲートウェイでデータを取得し、データベースにJSONで保存します。複数回に分けて、サンプルを用いて解説します。<br /> 前回は、PLCから取得したデータをデータベースに保存しました。</p> <p><a href="https://crieit.net/posts/PLC-JSON-5">PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (5)</a></p> <p>タイトルに書かれたテーマは前回で完了しています。完了していますが、これでは何か物足りないと感じ、今回はデータベースに次々と書かれるPLCからのデータをブラウザに表示してみます。</p> <p><a href="https://crieit.now.sh/upload_images/e6061bc3683ceff53f53e09554a0288e62dbf2a4188d4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e6061bc3683ceff53f53e09554a0288e62dbf2a4188d4.png?mw=700" alt="image" /></a></p> <h2 id="WEBアプリケーション"><a href="#WEB%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">WEBアプリケーション</a></h2> <p>データベースのデータをWEBブラウザに表示するために、WEBアプリケーションをサーバ上に作成します。このサンプルでは、WEBアプリケーションはPHPスクリプトで実装します。</p> <p>いよいよ図が窮屈になってきました。これ以上に窮屈な図は、もはや理解容易性の面で逆効果です。正確さと理解容易性は、ある時点以後、反比例します。</p> <p><a href="https://crieit.now.sh/upload_images/a96212b429a0ca11f28e750801258aea62dbf49dd5ca8.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a96212b429a0ca11f28e750801258aea62dbf49dd5ca8.png?mw=700" alt="image" /></a></p> <p>PHPである必要はありません。では、このサンプルを何故にPHPスクリプトで実装するのか?それは、そこ(私の開発環境)にPHPが稼働していたから。</p> <p>PHPの準備についてはここでは解説しません。PHPの準備についての解説は、他の記事にお任せします。PHPスクリプトでのMySQLアクセスについても、他の記事にお任せします。</p> <h2 id="HTMLとPHPスクリプト、そしてJavaScript"><a href="#HTML%E3%81%A8PHP%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%80%81%E3%81%9D%E3%81%97%E3%81%A6JavaScript">HTMLとPHPスクリプト、そしてJavaScript</a></h2> <p>結果的には、サーバ・サイドはPHPスクリプトのみではなく、以下の3つのファイルで構成してみました。</p> <ul> <li>HTML</li> <li>PHPスクリプト</li> <li>JavaScript</li> </ul> <p>3つめの JavaScript は jQuery です。jQuery は一般に配布されているとても便利なライブラリです。jQuery を利用することにより、とても高機能なWEBアプリケーションを短期間で実装することができます。jQuery の解説も他の記事にお任せします。</p> <p>以下がサンプルです。</p> <p>index.htm</p> <pre><code class="html"><html lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="Cache-Control" content="no-cache"> <title>Sample</title> <script type="text/javascript" src="js/jquery-3.6.0.min.js"></script> <script type="text/javascript"> function count_update() { $.ajax({ url:"index.php", method:"POST", success:function(data) { $('#count').html(data); } }); } $(function() { setInterval ( function() { count_update(); }, 1000 ); }); </script> </head> <body> count: <div id="count" style="display: inline-block;">count</div> </body> </html> </code></pre> <p>index.php</p> <pre><code class="php"><?php try { $dbh = new PDO('mysql:dbname=hoge;host=localhost;charset=utf8;' , 'hoge' , 'hoge001'); } catch (PDOException $e) { echo "Can't connect to database: " . $e->getMessage() . "\n"; exit(); } $sql = "SELECT JSON_EXTRACT(body, '$.value') AS count FROM from_plc ORDER BY time_insert DESC"; $res= $dbh->query($sql); foreach($res as $value) { echo $value['count']; break; } ?> </code></pre> <p>jQuery は、HTML中で参照されている、jquery-3.6.0.min.js です。<br /> HTML中には、jQueryを呼び出すオリジナルのJavaScriptスクリプトを書いています。<br /> いずれもコードの解説はいたしません。</p> <h2 id="WEBブラウザでアクセスしてみる"><a href="#WEB%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%A7%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">WEBブラウザでアクセスしてみる</a></h2> <p>WEBブラウザで上記の index.htm を参照すると以下ようになります。<br /> <a href="https://crieit.now.sh/upload_images/104c32d7402ec5f8908cc9021b18f35162dbfdbf6b841.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/104c32d7402ec5f8908cc9021b18f35162dbfdbf6b841.png?mw=700" alt="image" /></a><br /> 数字は、データベース上のテーブルが更新される都度変化してます。(正確には、JSONの要素"value"の値が変化してから1秒以内に、ブラウザの表示も変化します)</p> <h2 id="最後に"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB">最後に</a></h2> <p>ここまでサンプルとして実装したシステムを応用すると、工場の機器により製造される製品の生産数を、ほぼリアルタイムに画面表示する、といったシステムを構築することができます。<br /> 勘違いしていただきたくないのは、この記事で示したサンプルはあくまでもサンプルであって、基盤となるミドルウェアやプロトコル、プログラム言語は、この記事のサンプルで使用したものである必要はありません。もし、この記事で取り上げたような、PLCからデータを取得し、これを処理するようなシステムを設計・構築する場合は、この記事の内容にとらわれず、それぞれの環境、条件、実現したい機能などに応じて、最適なハードウェア、ミドルウェア、プロトコル、プログラム言語、そしてアーキテクチャを想像し選択してください。</p> <blockquote> <p>実はこのアーキテクチャを想像する、というのが設計者にとって最も楽しく重要な工程なのです。実装のテクニックなどは、アーキテクチャの上に成立する技術であって、アーキテクチャがイマイチなシステムは、おそらく実装もへんてこりんになってしまったり、どこかアンバランスなものになってしまうものです。</p> </blockquote> <p><a href="https://crieit.net/posts/PLC-JSON-1">PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (1)</a><br /> <a href="https://crieit.net/posts/PLC-JSON-2">PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (2)</a><br /> <a href="https://crieit.net/posts/PLC-JSON-3">PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (3)</a><br /> <a href="https://crieit.net/posts/PLC-JSON-4">PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (4)</a><br /> <a href="https://crieit.net/posts/PLC-JSON-5">PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (5)</a></p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/16042 2020-09-01T00:07:27+09:00 2020-09-01T02:54:31+09:00 https://crieit.net/posts/Raspberry-Pi-Ubuntu-SSH-A-to-B Raspberry Pi に Ubuntu を入れて SSH でログインするまでの A to B <p>何番煎じかわからないが、 Raspberry Pi (以下、 RasPi) に Ubuntu を入れる手順についてまとめてみようと思う。</p> <p>Ubuntu 公式の <a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/tutorials/how-to-install-ubuntu-on-your-raspberry-pi">How to install Ubuntu on your Raspberry Pi</a> チュートリアルは充実しているし、 単にインストールして動かすだけなら、既にある記事でも十分だろうとは思う。<br /> しかし、初回セットアップ時の細かいカスタマイズについて書かれているものがあまり見当たらなかったので、この記事ではそれについて補足しながらまとめていく。</p> <p>先に断っておくが、 cloud-init による IaC の話が中心になる。</p> <h2 id="TL;DR"><a href="#TL%3BDR">TL;DR</a></h2> <ul> <li>焼いた SD を RasPi に挿す<strong>前</strong>にブートプロセスの設定を書き換える</li> <li>cloud-init の挙動を理解しろ</li> <li>モニター無し & 無線LAN Only だと、工夫がいる</li> <li>ARP リクエストのブロードキャストが届かないと苦労する</li> </ul> <h2 id="きっかけ"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91">きっかけ</a></h2> <p>完全に私事だが、 おしごとで Linux 使うことが増えてきて、 WSL (or WSL2) や仮想マシン上の知識だけではなかなか難しい、ネットワーク回りの知識が不足してきた。<br /> そこで、勉強がてら何らかの物理的な Linux マシンが欲しかった。</p> <p>中古の x86 ラップトップに Linux を入れるのでも良かったのだが、 それだと Linux 向けの無線やネットワーク回りのドライバーが揃っているか確認するのが難しい。<br /> そこで、様々な Linux ディストリビューションが公式でサポートされ、 I/O も充実していて、 コスパの高い Raspberry Pi 4 B+ をチョイス。</p> <p>これまで、 RasPi 自体は所持していたのだが、 電子工作目的であったことから Raspberry Pi OS (旧 Raspbian) しか入れていなかった。<br /> より Linux サーバーらしい使い方も試してみたいと言うことから、今回は Ubuntu をいれることした。</p> <p>いざ、セットアップはじめようとして、致命的な問題に気づく。<br /> <strong>micro-HDMI のケーブルを持っていないと言うことに…</strong></p> <p>ということで、 なんとかセットアップを工夫して、 <strong>モニターが無くても一通り設定して ssh できる</strong> ところまで持って行くことを目標にする。</p> <h2 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h2> <p>用意するものは以下の通り。</p> <ul> <li>Raspberry Pi 2 または 3 または 4</li> <li>8GB 以上の microSD カード</li> <li>misroSD を書き込める PC (Win, Linux, mac)</li> <li>インターネットに繋がる Wi-Fi または イーサネットケーブル</li> </ul> <p>RasPi 無印 や Zero など、 SoC (CPU) が ARMv6 のものは、 Ubuntu ではサポートされていない。</p> <p>microSD カードは、 IOPS が速いものの方が、後々うれしいだろう。<br /> SD カードの規格で言えば、 アプリケーションパフォーマンスクラス A1 や A2 に対応しているものが良いというコトなのだろうが、正直モデル差やロット差が激しすぎるので、実際買ってみて IPOS が早いかどうか試してみるしかないと思う。</p> <p>以下のものも有れば望ましいが、今回の手順ではなくても大丈夫。</p> <ul> <li>HDMI (RasPi 4 の場合は micro-HDMI) で繋がるモニター</li> <li>RasPi 繋げる USB キーボード</li> </ul> <h2 id="ブータブル SD の作成"><a href="#%E3%83%96%E3%83%BC%E3%82%BF%E3%83%96%E3%83%AB+SD+%E3%81%AE%E4%BD%9C%E6%88%90">ブータブル SD の作成</a></h2> <p>まず、使用する Ubuntu の OS イメージを準備しよう。</p> <p>2020年8月時点では、 20.04 LTS と 18.04 LTS に対して、それぞれ 32bit版 と 64bit版 が存在する。</p> <p>特に理由が無ければ、 20.04 LTS の 64bit 版で良いだろう。<br /> 但し、 512MiB しか RAM がない RasPi 3A+ では、 64bit OS だと ssh でアクセスするだけで Out of Memory してしまうので、 32bit にすることをオススメする。<br /> また、 RasPi 2 の CPU は 32bit 版なので、 32bit OS しか選べない。</p> <p>microSD への書き込みは、 主に 以下の 2種類 の方法がある。<br /> どちらか好きな方でどうぞ。</p> <p>メモ:</p> <pre><code>以前は NOOBS という、 RasPi を起動後に OS 選択させてインストールさせるツールも存在したが、今回は触れない。 詳しくはググってほしい。 </code></pre> <h3 id="1. イメージファイルをダウンロードして 任意のツールで焼き込む"><a href="#1.+%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%97%E3%81%A6+%E4%BB%BB%E6%84%8F%E3%81%AE%E3%83%84%E3%83%BC%E3%83%AB%E3%81%A7%E7%84%BC%E3%81%8D%E8%BE%BC%E3%82%80">1. イメージファイルをダウンロードして 任意のツールで焼き込む</a></h3> <p><a href="https://crieit.now.sh/upload_images/cf06274a6f0865c95189a30c93ffa1195f4d1191446db.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cf06274a6f0865c95189a30c93ffa1195f4d1191446db.png?mw=700" alt="raspi-ubuntu-a-to-b-00.png" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/download/raspberry-pi">Install Ubuntu Server on a Raspberry Pi 2, 3 or 4</a> のページからダウンロードしたイメージファイルを、任意のツールで microSD に焼く方法。</p> <p>個人的には、 <a target="_blank" rel="nofollow noopener" href="https://www.balena.io/etcher/">balenaEtcher</a> を使って焼くのが、以下の理由でオススメ。</p> <ul> <li>.gz, .xz, zip などで圧縮された状態のイメージをそのまま焼ける <ul> <li>オンザフライで解凍しながら焼かれるので、あらかじめ展開しておく必要が無い</li> <li>... と思っていたけど、最近のバージョンでは一旦一時ファイルに展開されてから焼かれるっぽい。 それでも早いけど。</li> </ul></li> <li>Portable 版がある (インストール不要)</li> </ul> <h3 id="2. Raspberry Pi Imager を使う"><a href="#2.+Raspberry+Pi+Imager+%E3%82%92%E4%BD%BF%E3%81%86">2. Raspberry Pi Imager を使う</a></h3> <p><a href="https://crieit.now.sh/upload_images/19226279f917418413a8da2657fee0ee5f4d11a93a8bc.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/19226279f917418413a8da2657fee0ee5f4d11a93a8bc.png?mw=700" alt="raspi-ubuntu-a-to-b-02.png" /></a><br /> Raspberry Pi 財団公式サイトの <a target="_blank" rel="nofollow noopener" href="https://www.raspberrypi.org/downloads/">Raspberry Pi Downloadsi</a> ページからダウンロードできる、 Raspberry Pi Imager を使って、 OS のイメージをダウンロードしながら microSD に焼く方法。</p> <p>Raspberry Pi Imager をインストールして起動すると、 インストールしたい OS を尋ねられるので、 ここで Ubuntu の任意のバージョンを選択する。</p> <p>ダウンロードしながらそのまま焼き込むため、本来なら (1) よりも早くてオススメのハズ…<br /> …なのだが、少なくとも私が試した限り、 (1) のほうがずっと早いんだよねぇ。。。</p> <p>OS イメージを DL元 が違って、ダウンロード速度がネックになって遅くなっているのかな…<br /> あまり深く調べてないけど。</p> <h2 id="system-boot パーティション内の cloud-init 等の設定を書き換える"><a href="#system-boot+%E3%83%91%E3%83%BC%E3%83%86%E3%82%A3%E3%82%B7%E3%83%A7%E3%83%B3%E5%86%85%E3%81%AE+cloud-init+%E7%AD%89%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%92%E6%9B%B8%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B">system-boot パーティション内の cloud-init 等の設定を書き換える</a></h2> <p>作成した micorSD を早速 RasPi に挿す <strong>…前に</strong> 、 初回セットアップ設定の書き換えを行う。</p> <p>一度書き込んだ microSD を再び PC に挿すと、 <code>"system-boot"</code> という 250MB 位の FAT32 パーティションが開けるはずだ。</p> <p>このパーティション直下にある <code>README</code> というファイルを開くと、このパーティションに存在するファイルの概要が簡単に書いてある。</p> <pre><code class="text">An overview of the files on the /boot/firmware partition (the 1st partition on the SD card) used by the Ubuntu boot process (roughly in order) is as follows: * bootcode.bin - this is the second stage bootloader loaded by all pis with the exception of the pi4 (where this is replaced by flash memory) * config.txt - the first configuration file read by the boot process * syscfg.txt - the file in which system modified configuration will be placed, included by config.txt * usercfg.txt - the file in which user modified configuration should be placed, included by config.txt * start*.elf - the third stage bootloader, which handles device-tree modification and which loads... * uboot*.bin - various u-boot binaries for different pi platforms; these are launched as the "kernel" by config.txt * boot.scr - the boot script executed by uboot*.bin which in turn loads... * vmlinuz - the Linux kernel, executed by boot.scr * initrd.img - the initramfs, executed by boot.scr * meta-data - meta-data for cloud-init; usually just contains the instance id * network-config - network configuration for cloud-init; edit this to set up wifi access points and other networking settings * user-data - user-data for cloud-init; edit this to configure initial users, SSH keys, packages, etc. </code></pre> <p>このパーティションには、 Raspberry Pi の BIOS の変わりとなる <a target="_blank" rel="nofollow noopener" href="https://www.raspberrypi.org/documentation/configuration/config-txt/">config.txt, bootcode.bin, start.elf</a> と言った構成ファイルや、 ブートコード、 そして cloud-init の構成ファイルが入っている。</p> <p>このうち <strong>cloud-init</strong> というのは、 Infrastructure as Code (IaC) の一種だ。 その名の通り、 AWS 等のクラウド上の仮想マシンの初期設定を得意としている。<br /> 校正用の設定ファイル (YAML) を使って設定内容を記述することで、 クラウドやディストリビューションの違いをある程度吸収して初期設定を記述することができる。<br /> OS のイメージ内に予め cloud-init のサービス(デーモン) が組み込まれており、 OS の起動時に各クラウドのサービスの UI 経由で設定ファイルを読み取って、 主に初回起動時に初期設定処理が走る… というのが主な仕組みとなっている。</p> <p>RaspPi 用の Ubuntu Server イメージでは、この cloud-init 用の設定ファイルの取得元が上記の <code>system-boot</code> パーティションとなっている。<br /> イメージ組み込みの定義と <code>system-boot</code> パーティション内の定義を組み合わせて、 初期設定が実行される。</p> <p>例えば、 このイメージを使ってそのまま Ubuntu を起動すると、 "ubuntu" という ログイン ID に "ubuntu" というパスワードが設定され、 SSH のパスワード認証が有効になった状態で起動している。<br /> このうち、 "ubuntu" というユーザー名と SSH サーバーの起動は、 OS 組み込みの <code>/etc/cloud/cloud.cfg</code> の定義によるもの。<br /> そして、 "ubuntu" というパスワードの設定と、 SSH のパスワード認証の有効化は、 <code>system-boot</code> パーティションの <code>user-data</code> ファイル内の以下の定義によるものだ。</p> <pre><code class="yaml">#cloud-config chpasswd: expire: true list: - ubuntu:ubuntu ssh_pwauth: true </code></pre> <p>とうことで、 遠隔から SSH ログインできるように、 こららのファイルを順に書き換えていこう。</p> <h3 id="cloud-init の network-config"><a href="#cloud-init+%E3%81%AE+network-config">cloud-init の network-config</a></h3> <p>network-config ファイルには、 以下の 2つ の大きな役割がある。</p> <ul> <li>cloud-init 初期設定実行前にデバイスをネットワークにつなぐ</li> <li>netplan の <code>/etc/netplan/50-cloud-init.yaml</code> にファイルの内容を転記する</li> </ul> <p>『何だ、同じことじゃないか』 と思われるかも知れないが、この二つは システムに適用されるタイミング と言う点で大きく異なる。</p> <p>前者は、 cloud-init サービス の起動直後に、 cloud-init の通信を行うためにシステムに適用される。<br /> 一方後者は、後者は内容をファイルにコピーするだけである。</p> <p>cloud-init サービスによって netplan の <code>50-cloud-init.yaml</code> が書き換わるのは、ネットワークサービスの起動より後になる (systemd の <code>/usr/lib/systemd/system/cloud-init.service</code> の設定を参照)。<br /> このため、システムやサービスが再起動されるか、能動的に netplan apply しないと、 <code>50-cloud-init.yaml</code> の内容は適用されない。</p> <p>そしてもう一つ重要なのが、以下の一文。</p> <p><a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v2.html#network-config-v2">Networking Config Version 2 — cloud-init 20.3 documentation</a></p> <blockquote> <p>Cloud-init does not current support wifis type that is present in native netplan.</p> </blockquote> <p>そう。<br /> 前者では、 Wi-Fi の設定が無視されるのだ。</p> <p>すなわちこれは、 有線LAN が繋がっていない場合、 ファイルの更新や apt パッケージの取得などのインターネットに繋ぐような処理は、初回ブート時の cloud-init では全くできないことを意味する。<br /> 幸いにも、 Ubuntu イメージ組み込みの cloud-init の初期設定の内容は、ネットワークに繋がっていなくても処理が完走できる。</p> <p>cloud-init の元々の用途を考えれば、 Wi-Fi が設定できる必要性はないだろうし、 仕方が無いことなのかも知れないが……</p> <p>このファイルに Wi-Fi の設定を記載しただけでは、 初回起動時には Wi-Fi が有効にならないことは覚えておこう。<br /> 後の手順に関係してくる。</p> <p>とりあえず、 netplan の設定に Wi-Fi アクセスポイントの情報は書いておきたいので、 <code>network-config</code> は以下のように書き換える。<br /> SSID やパスワードは自分の環境のものに置き換えて書いてくれ。</p> <pre><code class="diff">--- network-config.org +++ network-config @@ -11,15 +11,15 @@ ethernets: eth0: dhcp4: true optional: true -#wifis: -# wlan0: -# dhcp4: true -# optional: true -# access-points: -# myhomewifi: -# password: "S3kr1t" +wifis: + wlan0: + dhcp4: true + optional: true + access-points: + "SSID-of-myhomewifi": + password: "password-of-myhomewifi" # myworkwifi: # password: "correct battery horse staple" # workssid: # auth: </code></pre> <h3 id="cloud-init の user-data"><a href="#cloud-init+%E3%81%AE+user-data">cloud-init の user-data</a></h3> <p>cloud-init 書き換えのメインとなる user-data。<br /> これは、 <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/topics/format.html#cloud-config-data">Cloud Config Data</a> の内容を記述する YAML 形式のファイルだ。<br /> 基本的には、 OS 組み込みの <code>/etc/cloud/cloud.cfg</code> の値を上書きしたり、 <code>cloud.cfg</code> で読み込まれた <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/topics/modules.html">Modules</a> の設定を記述していく。</p> <p>書き換え部分が長いので、3カ所に分けて説明していく。</p> <pre><code class="diff">--- user-data.org +++ user-data @@ -14,12 +14,22 @@ # expire user passwords chpasswd: expire: true list: - - ubuntu:ubuntu + - newusername:ubuntu + +# Override the default user name defined in cloud.cfg +system_info: + default_user: + name: newusername # Enable password authentication with the SSH daemon -ssh_pwauth: true +ssh_pwauth: false +ssh_authorized_keys: +- ssh-rsa AAAA<中略>== rsa-key-of-user1 + +# locale and timezone +timezone: Asia/Tokyo ## On first boot, use ssh-import-id to give the specific users SSH access to ## the default user #ssh_import_id: </code></pre> <p>既に上でも触れたが、 OS 組み込みの <code>cloud.cfg</code> の定義によって、 "ubuntu" というユーザーが作成されている。<br /> しかし、 既知のユーザー名をそのまま使うのはセキュリティ的にも若干不安があるし、 作成後のユーザー名を書き換えるのは若干面倒なので、作成されるデフォルトのユーザー名自体を、他の名前に書き換えてしまおう。<br /> <code>system_info.default_user</code> で、作成されるユーザ名を変更できる。<br /> また、 <code>chpasswd</code> によるパスワード書き換えの対象となるユーザ名も変更しておこう。</p> <p><code>ssh_pwauth</code> で、 ssh のパスワード認証が有効になっているので、 コレは切ってしまいつつ、<br /> 代わりに、 <code>ssh_authorized_keys[*]</code> リストに公開鍵を登録する。<br /> これで安全に ssh できるようになった。恵dc</p> <p>ついでにタイムゾーンも JST (Asia/Tokyo) に書き換えておこう。</p> <pre><code class="diff">--- user-data.org +++ user-data @@ -53,19 +63,20 @@ #- pastebinit #- [libpython2.7, 2.7.3-0ubuntu3.1] ## Write arbitrary files to the file-system (including binaries!) -#write_files: -#- path: /etc/default/keyboard -# content: | -# # KEYBOARD configuration file -# # Consult the keyboard(5) manual page. -# XKBMODEL="pc105" -# XKBLAYOUT="gb" -# XKBVARIANT="" -# XKBOPTIONS="ctrl: nocaps" -# permissions: '0644' -# owner: root:root +write_files: +- path: /etc/default/keyboard + content: | + # KEYBOARD configuration file + # Consult the keyboard(5) manual page. + XKBMODEL="pc105" + XKBLAYOUT="jp" + XKBVARIANT="" + XKBOPTIONS="" + permissions: '0644' + owner: root:root +- path: /etc/cloud/cloud-init.disabled #- encoding: gzip # path: /usr/bin/hello # content: !!binary | # H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA= </code></pre> <p>お次は、ファイルの作成。</p> <p>日本語キーボードを認識しないと面倒なので、 <code>/etc/default/keyboard</code> を書き換えて日本語キーレイアウトを認識させる。<br /> 但し、 Ubuntu Server では、このファイルを書き換えただけだとキーボードレイアウトの変更が有効にならないので、後述の <code>runcmd</code> で <code>dpkg-reconfigure</code> コマンドを対話無しのオプションをつけて実行しておく。</p> <p>cloud-init の設定の大半は、 初回ブート時にただ一度だけ実行されるのだが、 cloud-init サービス自体は基本的に毎回常に起動して、サービスとして常駐しっぱなしになっている。<br /> <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/topics/modules.html">Modules</a> で、 <code>Module frequency: per always</code> となっているモジュールの実行などは、ブートする度に毎回実行される。<br /> そうすると、場合によっては cloud-init の実行ログがログイン画面を覆ってしまうことが毎回の起動毎に発生して鬱陶しい。<br /> このため、 <code>/etc/cloud/cloud-init.disabled</code> という <a target="_blank" rel="nofollow noopener" href="https://cloudinit.readthedocs.io/en/latest/topics/boot.html?highlight=%22cloud-init.disabled%22#generator">空ファイルを作成しておく</a> ことで、 次回以降の起動時に cloud-init を無効にしておく。</p> <pre><code class="diff">--- user-data.org +++ user-data @@ -72,8 +83,19 @@ # owner: root:root # permissions: '0755' ## Run arbitrary commands at rc.local like time -#runcmd: +runcmd: +# apply keyboard configration +- [ dpkg-reconfigure, -f, noninteractive, keyboard-configuration ] +# apply netplan config defined on 'network-config' +- [ netplan, apply ] +# after wlan0 is connected, write the arp entry to target machine on background job +- [ nohup, sh, -c, 'sleep 30; ping 192.168.0.2 &' ] #- [ ls, -l, / ] #- [ sh, -xc, "echo $(date) ': hello world!'" ] #- [ wget, "http://ubuntu.com", -O, /run/mydir/index.html ] + +# Restart 2 minutes after running all config modules (for apply keyboard configration) +power_state: + delay: '+2' + mode: reboot </code></pre> <p>最後に、任意のコマンドの実行。</p> <p>最初の <code>dpkg-reconfigure</code> は、 <code>keyboard</code> の書き換えを反映させるため。</p> <p><code>[ netplan, apply ]</code> のコマンドで初めて、 <code>network-config</code> で設定した Wi-Fi 設定が有効になる。</p> <p>最後の <code>[ nohup, sh, -c, 'sleep 30; ping 192.168.0.2 &' ]</code> は説明が難しいのだが、これは仮に ssh でゲストの IP アドレスが <code>192.168.0.2</code> であると仮定して、そこへ向かって Ping を打っている。<br /> 何故そのようなことをしているかというと、 ゲストPC から RaspPi の IP アドレスを ARP コマンドで確実に調べられるようにするためだ。</p> <p>RasPi に繋げるモニターがないことを前提とすると、 SSH するために DHCP によって RasPi に割り当てられた IP アドレスをどうやって調べるのか、と言う問題に直面する。</p> <p><a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/tutorials/how-to-install-ubuntu-on-your-raspberry-pi#4-boot-ubuntu-server">How to install Ubuntu on your Raspberry Pi #4</a> に書かれているように、 RasPi の MAC アドレスの先頭 24bit は b8-27-eb (Pi 2~3) もしくは dc-a6-32 (Pi 4) と決まっている。<br /> 通常、 RasPi がオンラインになったときに、 ARPリクエストがブロードキャストされるため、 同一ネットワーク内の PC の ARP テーブルに RasPi のレコードが追加され、 以下のような arp コマンドで対象の IP アドレスを調べることができるようになる。。。 <strong>多くの場合は。</strong></p> <pre><code class="powershell">while(1) { arp -a | ?{ $_ -match 'dc-a6-32' }; Start-Sleep -Seconds 1} </code></pre> <p>ところが、 RasPi が Wi-Fi で繋いでいるのがルーターだったりすると、 ルーターが気を利かせて、 同じ サブネット内であっても ARPリクエストを代替して投げてしまい、 同じサブネットの端末全員の ARP テーブルには、 RasPi の MAC アドレスのレコードが記録されないことがある。</p> <p>そこで、 RasPi 側から SSH のアクセス元となる PC を指定して ping を投げることで、 その PC の ARP テーブルに確実に RasPi が載るようにしている。</p> <p>ちなみに、 30秒 sleep しているのは、 <code>netplan apply</code> した直後はまだ Wi-Fi に繋がっていないことが多いためだ。</p> <p>そして最後に、 cloud-init の停止や タイムゾーンの変更を確実に反映するため、 <code>power_state</code> で再起動させている。</p> <h2 id="RasPi の config.txt"><a href="#RasPi+%E3%81%AE+config.txt">RasPi の config.txt</a></h2> <p>cloud-init とは関係ないが、 必要があれば <a target="_blank" rel="nofollow noopener" href="https://www.raspberrypi.org/documentation/configuration/config-txt/">config.txt</a> の構成ファイル書き換えてしまおう。</p> <p>例えば、 HDMI の解像度が一般的でなかった場合や、 端末とのネゴシエーションがヘタクソで適切な解像度で表示できない場合などは、 このファイルで HDMI の解像度を指定してやる必要がある。</p> <p>参考:<br /> * <a target="_blank" rel="nofollow noopener" href="https://qiita.com/god19/items/da8ce82b93b36dd5489d">Raspberry Pi 4B 画面解像度設定(TV出力版) - Qiita</a><br /> * <a target="_blank" rel="nofollow noopener" href="https://www.raspberrypi.org/documentation/configuration/config-txt/video.md">Video options in config.txt - Raspberry Pi Documentation</a></p> <p>但し、 <code>config.txt</code> には、</p> <pre><code class="conf"># Please DO NOT modify this file; if you need to modify the boot config, the # &quot;usercfg.txt&quot; file is the place to include user changes. </code></pre> <p>って書いてあるので、 書き換えるなら <code>config.txt</code> ではなく <code>usercfg.txt</code> にしておくと良い。</p> <pre><code class="diff">--- usercfg.txt.org +++ usercfg.txt @@ -1,3 +1,9 @@ # Place "config.txt" changes (dtparam, dtoverlay, disable_overscan, etc.) in # this file. Please refer to the README file for a description of the various # configuration files on the boot partition. + +framebuffer_width=1024 +framebuffer_height=600 +hdmi_group=2 +hdmi_mode=87 +hdmi_cvt=1024 600 60 1 0 0 0 </code></pre> <h2 id="起動する"><a href="#%E8%B5%B7%E5%8B%95%E3%81%99%E3%82%8B">起動する</a></h2> <p>こんな感じで一通りファイルを書き換えたと、 RasPi に microSD をさして起動すると、初回ブート時の設定を適用しながら起動してくれる。</p> <p>cloud-init の処理が終わらなくても、 ログイン画面には進み、 その後も cloud-init のサービスは稼働し続ける。<br /> このため、 最初の起動でログイン可能になったとしてもログインせず、 全ての処理が終わって一度再起動されるのをおとなしく待つ方が良い。</p> <p>RasPi 3A+ の場合、全ての処理が終わるのに5分以上ったので、気長に待つようにしよう。</p> <p>前述の arp で RasPi の IPアドレスを調べて、そこに対して SSH できるようになっていれば、とりあえずは完成だ。</p> <p>cloud-init についてもう少し掘り下げて説明しようと思ったのだが、記事が長くなってしまったので、次回の更新に回す。</p> advanceboy