Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8835 articles
Browse latest View live

ちょっと古いBraviaもHey! Siriで操作できるようにする。

$
0
0

はじめに

ほんの少し前のことなんですが。
長年使ってたテレビが遂に壊れてしまったんですよ。地デジが始まる頃に買った『KDL-40X5000』って言うソニーのテレビ。
テレビが映らないのは困るなあってんで、すぐに買いに行ったんです。そしたらこの10年ちょっとで、テレビってえらい進化しているんですね。薄く軽くなったのはもちろん、ベゼルが小さくなったんですよ。なので、設置面積はほとんど変わらずだいぶ大きなテレビを置けました。
今回買ったのは『KJ-49X8500F』です。
Android TVなんですねー。
YouTubeとか見られるんですねー。
あこがれの4Kですよー。
画が綺麗になりましたねー。
画面も大きくなりましたよー。
と大満足。

そこにニュースが。
ソニーのAndroid TVがAirPlay 2とHomeKitをサポート」(engadget)
やったー!テレビがSiriで動かせるぞ!
ん?

アップデートが提供されているのは、2018年モデルのZ9FとA9Fシリーズ、2019年モデルのZ9G、A9G、X950G、X850Gシリーズ。

Gシリーズ?
:tired_face::tired_face::tired_face:僕の「F」じゃん! ほんのちょっと前に買ったのに対象外じゃん!

前置きが長くなりました。
たった一世代で泣いたことがすごく悔しかったので、自力でSiriで操作できるようにしました。
たぶんAndoroid TVの機種であれば、さらに前の機種でも同じだと思います。

この記事でやることとセールスポイント

  • Raspberry PiにHomebridgeを入れ、braviaプラグインで操作可能にします。
  • homebridge-braviaプラグインのReadMeには書かれていない日本国内のチャンネルの選択方法を記載します。
  • それだけだと出来ないSiriによるチャンネル変更も(力業で)実現します。
  • BRAVIA(テレビ)が持っているAPIを利用します。赤外線スマートリモコンとかは使いません。

今回登場する…

ハードウェア

これらはすべて同じLANに繋がっている必要があります。

  • Raspberry Pi Zero WH (ずっと前に買って転がってた)
    • Raspbian buster
  • Sony Bravia KJ-49X8500F
    • Android 8.0.0にアップデート済み
  • Homeアプリが使えるiOS / iPadOS / macOSどれかの搭載機

ソフトウェアやら

  • node.js
    • homebridge(nodeModule)
      • homebridge-config-ui-x
      • homebridge-bravia
      • homebridge-cmd4
    • Express(nodeModule)
    • bravia(nodeModule)
    • node-cron(nodeModule)
  • bash
  • jq

とりあえずRaspberry PiのOSなどはインストール済みで、LANに接続しているモノとします。
この記事では pizero.localで接続出来るようになっている、と仮定しますね。

Pi Zeroに最新のnode.jsを入れる

RaspberryPi Zero のCPU は ARMv6 のため、node.js公式からはv11以降、バイナリが配布されていません。現時点での最新はv14.8.0。LTS版はv12系。後述のhomebridgeでもLTS版を推奨していますが、バイナリの配布はARMv7とARMv8だけです。
とは言えbuildするのもめんどうなので、アンオフィシャルビルドから入れちゃいます。
正直ちょっと古くても大丈夫ですが、node.jsはバージョン変更のトラブルが多いので、v12系の方が安心かと。
まあ、趣味の範囲です。

$ sudo npm install-g n #バージョン管理ツールのnを入れます。$ sudo n latest #最新のnodeを入れてみます。$ node -v
v11.15.0 #ちょっと古いのが入りました。 $ sudo-i#ミラー先をアンオフィシャルに変更するために環境変数をセットします。$ export N_NODE_MIRROR=https://unofficial-builds.nodejs.org/download/release/
$ sudo-E n latest #いまセットした環境変数を使って、試しにnode最新版を入れてみます。$ node -v
v14.8.0  #入りました。今度はちょっと新しすぎます。$ sudo-E n 12 #12系の中で最新のものをインストールします。
  installing : node-v12.18.3
       mkdir : /usr/local/n/versions/node/12.18.3
       fetch : https://unofficial-builds.nodejs.org/download/release/v12.18.3/node-v12.18.3-linux-armv6l.tar.xz
   installed : v12.18.3 (with npm 6.14.6)$ node -v
v12.18.3  #LTSバージョンが入りました。 # どのバージョンがLTSかわからなかったら n lts でも大丈夫。

homebridge および homebridge-config-ui-xを入れる

homebridgeは入っている前提で良いかもしれませんが、簡単に記載します。
また、homebridge-config-ui-xはhomebridgeの設定をWebブラウザから出来るようになるプラグインです。便利なので標準のつもりで入れています。
詳しくはこちら。
Homebridge & Systemd (Raspbian, Ubuntu, Debian)

$ sudo npm install-g--unsafe-perm homebridge homebridge-config-ui-x
$ sudo hb-service install--user homebridge

これでOKです。
homebridge-config-ui-xとhb-service(httpサーバ)の組み合わせで起動すると、
http://pizero.local:8581/
でオシャレ設定画面にアクセス出来ます。
スクリーンショット 2020-08-18 16.58.39.png
初期アカウントとパスワードは両方ともadminです。

プラグイン:homebridge-braviaを入れる

homebridgeにはBraviaを操作するプラグインが複数あります。
今回はそのものズバリな名前の「homebridge-bravia」を使ってみます。

これはhomebridge-config-uiで簡単に入れられます。
プラグインのタブでbraviaを検索。「homebridge-bravia」を見つけたら、インストールを押すだけです。
スクリーンショット 2020-08-18 16.59.20.png

homebridge-braviaはconfig-uiに対応しているので、主な設定は設定UIで可能です。
とりあえず[設定]を押して設定してみて下さい。
configはあとで手で修正する必要があるのですが、とりあえずひな形が出来るので便利です。

NameはSiriに呼びかける名前になるので注意して下さい。
ここで「おばあちゃん」とかわかりにくい名前を付けると
「Hey siri, おばあちゃんを消して」とか言う必要があります。家族がギョッとします。
僕は無難にテレビにしておきました。
IP / Hostnameは テレビのものを。
Android.localでも大丈夫な気がしたんですが、どうも上手く行きませんでした。
これはいつか検証する必要があります。とりあえずIPアドレスで指定。
TV Sourceは後述。とりあえず適当に選択して下さい。
それ以外は特に注意点は無いので、デフォルト値か空欄でOKです。
PIN Entry Server Portと言うちょっと意味がわからないのがありますね。
これもデフォルトの8999のままにしておきます。あとで説明します。

保存したら、上部のメニューから「コンフィグ」を選んでconfigファイルを直接編集します。

日本国内におけるTV Sourceの設定について

homebridge-braviaのREADMEには、TV Sourceについて以下のように記載があります。

入力設定値
アンテナtv:dvbt
ケーブルtv:dvbc
衛星tv:dvbs

これはヨーロッパなどで使われている規格です。日本国内ではこの設定は正しくありません。

入力設定値
地デジtv:isdbt
BStv:isdbbs
CStv:isdbcs

とする必要があります。
詳しく知りたい方は ISDB (wikipedia)をご覧下さい。1(北米では 2
結果、config.jsonのbravia部分は、おおよそ以下のようになります。
tvsourceには地デジとBSを指定しました。僕はCS見てないので。
applicationsの部分はTVから起動出来るアプリです。
下の方にあるsourcesはhomeアプリ内に表示したい入力の一覧です。cecを指定しておくと、HDMIに繋がった機器の名称が並びます。HDMI-CECに対応していない機種がある場合はextInput:hdmiを指定することになります。その場合はHDMI-1などの番号で表示されます。

config.json
{"tvs":[{"name":"テレビ","ip":"10.0.1.xx","externalaccessory":false,"soundoutput":"speaker","tvsource":["tv:isdbt","tv:isdbbs"],"applications":[{"title":"Prime"},{"title":"YouTube"}],"sources":["tv:isdbt","tv:isdbbs","extInput:cec"]}],"platform":"BraviaPlatform"},

ここまでで、「Hey siri! テレビを点けて!」が出来るようになりました!
テレビのスイッチを付けてから、
右上にある「再起動」ボタンを押してhomebridgeを再起動しましょう!

テレビの前へ急げ

再起動しましたか?
再起動すると、テレビの画面に数字が表示されるはずです。これはPINコード。この番号を知らない機械からの命令は受け付けないよ!と言う意味です。PINコードの有効期限は60秒!慌ててhomebridgeを設定しているPCに戻って下さい。
少し前に「デフォルトでOK」と書いた「 PIN Entry Server Port」を憶えてますか?デフォルトのままだったら8999なので、
http://pizero.local:8999/
にアクセスして、さっき憶えたPINコードを入力して下さい。
間に合わなかったらもう一回homebridgeの再起動をやり直し!

Hey siri! テレビを点けて!

さて、無事に立ち上がったでしょうか?
iOSまたはmacOSで、homeアプリを立ち上げましょう。
テレビ(または おばあちゃん)の名前の付いたアイコンが表示されましたか?
iOSの場合は長押し(macOSの場合はコンテクストメニューから「コントロールを表示」)で、テレビを操作するためのUIが表示されるはずです。
IMG_0044.png

丸い大きなスイッチで、テレビのON/OFFが出来ます。ボタンを押してみて下さい。テレビが消えましたか?
次に、試しに口に出してみましょう 「Hey siri, テレビを点けて!」

テレビが点きましたか? おめでとうございます。

Homeアプリのスイッチの下には、チャンネルが選択出来るメニューが表示されます。
表示されませんか?
チャンネルの一覧を取得するのに少し時間が掛かるようです。
でも、少し待てば表示されます。

いくら待っても表示されない?
右下の歯車を押してみて下さい。
「アクセサリ」の欄に大きな数字があり、それを選ぶと中にチャンネルの名前のアイコンが並んでいますか?
これはダメな状態。この状態だと、並んだチャンネルを選んでもなにも起きません。
どうやらこれは不具合なんじゃ無いかと思います。
こちらにも報告されていますが、この状態になってしまったら、一度Homeアプリを再起動してみて下さい。
iOSのAppスイッチャーからスワイプして完全に消してしまい、もういちど起動します。
するとメニューの中に現れる(ことが多い)ようです。

Hey siri! フジテレビを点けて!

さて。メニューの中に無事チャンネルが現れたでしょうか?
地域にもよりますが、サブチャンネルまで含めて全部表示されるはずです。
メニューから放送局を変更すると、テレビのチャンネルが変わるはずです。やったね!
でも数が多いとちょっと変更するのシンドイですね。
では、口に出してみましょう。 「Hey siri, フジテレビを点けて!」

おや。
だめですね。
ダメですよね?

そうなんです。どうもsiriは、メニューの中にある項目を変更出来ないみたいなのです。
どうもこれは、iOSの(homekitの?homeアプリの?siriの?)仕様である様子。
これではせっかく声でテレビを点けても、結局リモコンを手に持つか、homeアプリを立ち上げて小さなメニューをくるくるしなければいけません。
ぐぬぬぬぬ。悔しい。
「Gシリーズ」ではそれが出来るのかどうかサッパリわからないけど、出来ないのは悔しい。。。

そんなわけで、なんとかして音声でチャンネルが変えられるように挑戦します。
テレビが点けば良いだけの場合は、ここで記事を読むのをやめることが出来ます。

nodeでBraviaを操作する

homebridge-braviaでは、iPhoneのhomeアプリからテレビの電源オンオフと、チャンネルの変更が出来ました。
siriに頼んでテレビのオンオフも出来ました。
しかし「Hey siri, チャンネルをTOKYO MXにして!」が出来ない。
ちょっと別のアプローチを取ることにしました。

bravia node_moduleを入れる

なんと、Braviaを操作できるnodeモジュールが存在します。
bravia npm
このnodeモジュールでは、テレビに対してかなりいろいろな操作が可能です。
(homebridge-bravia-simpleなどのhomebridgeプラグインはこれのWrapperのようです)
これを使ってみることにします。

まずディレクトリを作ってそこにmoduleを入れます。

$ mkdir ~/homebridge/tvChannels &&cd$_$ npm init
$ npm install bravia

まず、braviaモジュールのページを参考に、TVの設定をして下さい。
テレビのリモコンを持ち、ホームを押してから右上の歯車【設定】を選ぶと設定が可能です。
Pre-Shared Key (PSK) と言うのは自分で決める暗証番号のようなもの。
ここでは8888と設定したと仮定します。

では実験してみましょう。
次のコードを書いて保存してください。IPアドレスとPSKを自分のモノに変更することを忘れずに。

getContentList.js
constBravia=require('bravia');bravia=newBravia('10.0.1.X',80,'8888');bravia.avContent.invoke('getContentList','1.0',{"source":"tv:isdbt",'stIdx':0,"cnt":200}).then(contentList=>{for(vari=0;i<contentList.length;i++){varcontent=contentList[i];console.log("[Channel]:"+content.dispNum+"\n"+"[Title]:"+content.title+"\n"+"[URI]:"+content.uri+"\n ----");}});

実行します。
$ node getContentList.js
チャンネル一覧が出てくれば正常に動いています。
気が付いていると思いますが、3行目のsource にある tv:isdbtをtv:isdbbsにすれば、BS放送の一覧が出てきます。
出力結果の1行目。dispNumの値は三桁のチャンネル番号です。あとでチャンネルを指定する際にこの数字を使うので、自分がみたい局の番号は控えておいて下さい。
また、出力結果の3行目にあるURIがチャンネルを変更するときに必要になるのですが、それはあとで。

3行目で呼び出しているgetContentListは、Braviaが持っているAPI
その詳細は
BRAVIA Professional Display Knowledge Center
ここで公開されています。これは「法人向けブラビア」用に用意されたページのようですが、一般家庭向けのモノでも使えるようです。たぶん。
第二引数にはバージョンナンバーを指定します。指定しなくても動きますが、複数のバージョンが用意されているAPIではこれで切り替えます。
さっきのは、avContent サービス のgetContentList APIを叩きました。
バージョン1.5まで用意されています。バージョン番号を変更して叩き直すと、レスポンス内容が変わるので遊んでみてください。

次はappControl サービス のgetApplicationList APIを叩いてみましょう。

getApplicationList.js
constBravia=require('bravia');bravia=newBravia('10.0.1.X',80,'8888');bravia.appControl.invoke('getApplicationList').then(appList=>{for(vari=0;i<appList.length;i++){varapp=appList[i];console.dir(app);}});

インストールされているアプリの一覧が出たりします。
これはアイコン画像とか提供されていてちょっと面白いです。

なお、現在受信している番組の情報はavContent サービス のgetPlayingContentInfo APIで取得出来ます。
日本国外だったりケーブルテレビだったりで、tv:isdbtではない場合はこれで調べることが出来ます。

getPlayingContentInfo,js
bravia.avContent.invoke('getPlayingContentInfo','1.0').then(nowPlayContent=>{console.dir(nowPlayContent);});

【メモ】
実際のところbraviaと言うnodeモジュールは、このAPIを叩いているだけだ。
APIドキュメントに従って、
$ curl -s -X POST -H "Content-Type: application/json; charset=UTF-8" -H "X-Auth-PSK: 8888" -d '{"method": "getApplicationList", "id": 60, "params": [], "version": "1.0"}' http://10.0.1.X/sony/appControl | jq .
とかやると同じ結果を得ることが出来る。憶えておいても損はない。
またBraviaのAPIは、おおよそテレビで行いたい操作は網羅されている上に、赤外線リモコンをエミュレートするものもある。だいたい何でも出来る。

さてこれで、Raspberry PiからBRAVIAを操作する目処が立ちました。
基本的な流れはこんな感じでしょうか。

  1. フジテレビを点けるように命令
  2. 電源が入っているかを確認 system > getPowerStatus
  3. 入ってなければ電源を点ける system > setPowerStatus
  4. フジテレビのURIを取得 avContent > getContentList
  5. そのURIにチャンネルを変更 avContent > setPlayContent

あと現在放送中のチャンネルを知る avContent > getPlayingContentInfo
が必要になります。

必要なAPIはすべて存在しているようです。
しかし問題は、一番最初の「点けるように命令」ですね。

プラグイン homebridge-cmd4を使う

Raspberry PiからBraviaの操作ができることがわかったので、次はhomekitでの操作を考えます。
これには、homebridge-cmd4と言うプラグインを使うことにしました。homekit対応アクセサリーをエミュレートし、shコマンドを発行出来るモノ、だと思えばよろしいです。
とりあえず入れちゃいましょう。
homebridge-config-ui-xから検索してインストールします。

手作業で入れるならこうなります
$ sudo npm install-g--unsafe-perm homebridge-cmd4

これを使って、homekitアクセサリーの「スイッチ」を作ります。

siriにお願いすると、スイッチがオンになります。
スイッチをオンにするとRaspberry Piがコマンドを発行し、BRAVIAを操作します。
スイッチに「フジテレビ」とか「TOKYO MX」とかの名前を付けておけば、「Hey siri, フジテレビを点けて」が出来るようになるというわけです。

結果としてチャンネルの数だけ「スイッチ」が並んでしまい、もうどうにかならんのかと言う状況になるが、今のところこれが唯一の解決方法。
改良方法を思いついたらチャレンジすることとします。

僕はチャンネルを20個用意しました。
20個のボタンが並ぶとhomeアプリが専有されてしまうので、「チャンネル」と言う名前の部屋を作りました。
正直、なんかもうちょっと良い方法無いのかよ、とは思ってはいます。

homebridge-cmd4でスイッチを作った場合、
スイッチをオンにした際に発行されるsetコマンドと、
定期的に現在の状況を得るgetコマンド
発行されます。

setコマンドが発行されたらチャンネルを変更します。
フジテレビが点いている状態でテレビ朝日を選ぶと、一時的に両方ともスイッチがオンになりますが、次にgetコマンドが発行されたタイミングでフジテレビがオフになるわけです。
タイムラグが出てしまいますが、まあ良しとしよう。

BRAVIAコントロールサーバを作る

先ほどのnodeモジュールを元に、RestAPI形式のサーバにしました。
まあ、扱いやすいので。そのため、nodeモジュールのExpressを利用しました。
一部cron処理したいのでnode-cronも入れました。
これは普通のcrontabを使っても、settimeout()を使っても良いのですが。

$ cd ~/homebridge/tvChannels
$ npm install express
$ npm install node-cron

nodeサーバスクリプト

次にサーバになるスクリプトを用意しました。
長いし汚いですが全文記載します。
参考に好きなように書き換えて下さい。

サーバスクリプト(node index.js)はこちらをクリック(長いので折りたたみました)
index.js
'use strict';//必要なモジュール//Expressvarexpress=require('express');varapp=express();varport=3000;//ブラビアconstBravia=require('bravia');varbravia=newBravia('10.0.1.X',80,'8888');//自分の環境に合わせてください//ユーティリティ系constutil=require('util');constfs=require('fs');constcron=require('node-cron');//設定ファイルなど 自分の環境に合わせてください//チャンネルリストconstchannelInfoFile="/home/pi/homebridge/tvChannels/channelInfo.conf";//現在のチャンネルconstcurrentChannelCache="/home/pi/homebridge/tvChannels/channelCache.txt";varcurrentChannel;//定期実行//放送局一覧更新(必要なの?) 毎時実行cron.schedule('0 * * * *',()=>refreshCannelList());//現在チューニングされているチャンネルの情報更新:30秒ごとcron.schedule('*/30 * * * * *',()=>getChannelToBravia());//API サーバ//現在チューニングされているチャンネル情報の取得app.get('/channel',function(req,res){getChannel().then(nowChannel=>{res.json(nowChannel);});});//与えられたチャンネルが現在チューニングされているチャンネルかの確認//cmd4から Get で呼ばれるよapp.get('/channel/:channel',function(req,res){letchannel=req.params.channel;ckChannel(channel).then(ckResult=>{console.log(JSON.stringify(ckResult));res.json(ckResult);});});//与えられたチャンネルに変更する//cmd4から Set で呼ばれるよapp.put('/channel/:channel',function(req,res){letchannel=req.params.channel;//与えられたチャンネルletresult=setChannel(channel);//チャンネル変更res.json(result);});app.listen(port);console.log("START Bravia Server");varisset=function(data){//便利関数if(data===""||data===null||data===undefined){returnfalse;}else{returntrue;}};//ブラビア操作//与えられたチャンネルに変更するfunctionsetChannel(channel){letresult;//保存済みの Channelリストファイルから uri を取得util.promisify(fs.readFile)(channelInfoFile,'utf-8').then(channels=>{channels=JSON.parse(channels);if(channels[channel]!==undefined){leturi=channels[channel].uri;returnuri;}else{thrownewError('Channel:'+channel+' is not exist.');}}).then(uri=>{//リストを取得したあとでbraviaPowerOn()//テレビの電源がオフなら点ける.then(powerOnResult=>{//電源が点いたあとでbravia.avContent.invoke('setPlayContent','1.0',{"uri":uri})//チャンネルを変える.then(setPlayContentResult=>{getChannelToBravia();//チャンネル情報を更新しておくresult={"action":"setChannel","result":"success","message":"Turn Channel to "+channel};returnresult})});}).catch((error)=>{//エラーresult={"action":"setChannel","result":"error","message":error};console.error(result);})}//現在チューニングされているチャンネル情報の取得//キャッシュされた情報から返答functiongetChannel(){returnnewPromise(function(resolve){if(isset(currentChannel)){//現在チャンネルが変数にあればそれを返すresolve(currentChannel);}else{//変数がセットされてないときはutil.promisify(fs.readFile)(currentChannelCache,'utf-8')//キャッシュファイルを見る.then(cache=>{currentChannel=JSON.parse(cache);if(isset(currentChannel)){resolve(currentChannel);}else{//キャッシュファイルも空だったらgetChannelToBravia()//BRAVIAに聞きに行く.then(currentChannel=>{resolve(currentChannel);});}}).catch(err=>{//ファイルが存在しないときgetChannelToBravia()//BRAVIAに聞きに行く.then(currentChannel=>{resolve(currentChannel);});});}});}//現在チューニングされているチャンネル情報をテレビに問合せて保存するfunctiongetChannelToBravia(){returnnewPromise(function(resolve){bravia.avContent.invoke('getPlayingContentInfo','1.0').then(currentChannelFromBravia=>{fs.writeFile(currentChannelCache,JSON.stringify(currentChannelFromBravia,null,2),"utf-8",(err)=>{currentChannel=currentChannelFromBravia;resolve(currentChannel);});}).catch(function(){//電源が入ってないと失敗するので無視するconsole.log('[BRAVIA Power Off]');});});}//チャンネル確認リクエストfunctionckChannel(ckChannel){returnnewPromise(function(resolve){letresult;bravia.system.invoke('getPowerStatus','1.0')//電源チェック.then(power=>{if(power.status!=='active'){//電源が入ってないときresult=false;letresultJSON={"result":result,"checkChannel":ckChannel,"nowChannel":"powerOff"};resolve(resultJSON);}else{//電源が入っているときgetChannel().then(nowPlayContent=>{if(nowPlayContent.dispNum===ckChannel){result=true;}else{result=false;}letresultJSON={"action":"ckChannel","result":result,"checkChannel":ckChannel,"nowChannel":nowPlayContent.dispNum};resolve(resultJSON);});}});});}//電源を取得して OFF なら ON にするfunctionbraviaPowerOn(){returnnewPromise(function(resolve){bravia.system.invoke('getPowerStatus','1.0').then(power=>{if(power.status==='active'){resolve(power);}else{bravia.system.invoke('setPowerStatus','1.0',{"status":true}).then(result=>{resolve(result);})}});})}// Bravia から Channelリストを取得して保存する// 一覧はテレビの電源が入って無くても取れる様子functionrefreshCannelList(){console.log('[Channel List Refresh Start]');Promise.all([bravia.avContent.invoke('getContentList','1.0',{"source":"tv:isdbt","stIdx":0,"cnt":200}),//地デジリストbravia.avContent.invoke('getContentList','1.0',{"source":"tv:isdbbs","stIdx":0,"cnt":200})//BS リスト//お好みに応じて CS や HDMI なども追加]).then(([isdbt,isdbbs])=>{letchannelInfo=makeChannelInfo([isdbt,isdbbs]);fs.writeFile(channelInfoFile,JSON.stringify(channelInfo,null,2),"utf-8",(err)=>{if(err){console.log(err);}else{console.log('[Channel List Refreshed]')}});})}// Bravia から取得したリストを成形するfunctionmakeChannelInfo(channels){letchannelInfo={};for(vari=0;i<channels.length;i++){for(varx=0;x<channels[i].length;x++){channelInfo[channels[i][x].dispNum]=channels[i][x];}}returnchannelInfo;}

【補足メモ】
Braviaが返すuriにはtripletStrと言う三つの数字が連結されたものが含まれています。
これがなんなのかわかりません。ずっと固定なのか定期的に変更されるのかなどの仕様ががわからないですが、チャンネルを変更するたびに問い合わせるのは効率が良いとは思えないので、サーバ上に保存しておいて定期的に更新することにしました。

また、一度作ってみたところ、20個ほどあるすべてのボタンが、自分のステータスがオンかオフかを確認するために、数十秒に一回アクセスしに来ます。。その都度テレビに通信が行くのは精神衛生上よろしくなく、また頻繁にタイムアウトエラーも出てしまっていたので、BRAVIAサーバが定期的に取得しに行き、ボタンたちはキャッシュされたモノを見ることにしました。
(それでもたまにタイムアウトエラーが起きている様子)
登録しているチャンネルの数やサーバに使っているマシンのスペックなどを見ながら確認しに行く間隔を調整してください。(間隔の設定は後述のcmd4のconfigで行う)

さらに、これを常に起動させるためにServiceに登録します。
ホントはユーザー権限で入れるのがお行儀良いのだと思いますが、あんまり気にしない。

/etc/systemd/systemに以下のファイルを作ります。sudoしてね。

$ sudo vi /etc/systemd/system/bravia.service
bravia.service
[Unit]
Description = SonyBRAVIAControlServerAfter = network.target 

[Service]
ExecStart =node /home/pi/homebridge/tvChannels/index.jsRestart = alwaysType = simple

[Install]
WantedBy = multi-user.target

置いたらサービスに登録します

$ sudo systemctl start bravia #起動出来るかな?$ systemctl status bravia #動いてるか確認$ curl http://localhost:3000/channel #動いていればこれで今受信中の番組情報が出る$ sudo systemctl enable bravia #大丈夫そうなら次回以降も自動起動するように登録 

これで、Raspberry PiがBRAVIA操作サーバになりました:thumbsup_tone2:

cmd4の設定

続いて、cmd4の設定を行います。
homebridge-config-ui-xに戻って下さい。
画面上部の【コンフィグ】を押しましょう。

このJSONの中に、以下のセクションを追加します。

config.json
{"platform":"Cmd4","name":"Channel","accessories":[{"type":"Switch","name":"NHK","state_cmd_suffix":"011","on":false,"state_cmd":"/home/pi/homebridge/tvChannels/bravia.sh","polling":[{"on":true,"interval":20,"timeout":8000}]},{"type":"Switch","name":"Eテレ","state_cmd_suffix":"021","on":false,"state_cmd":"/home/pi/homebridge/tvChannels/bravia.sh","polling":[{"on":true,"interval":20,"timeout":8000}]},………以下必要なチャンネルだけ続ける]}

アクセサリーaccessoriesとしてスイッチ"type": "Switch"をたくさん作っていきます。
チャンネルの名前も"name":"NHK"付けてあげます。この名前でチャンネルを決めます。
起動した直後の設定はオフ"on": falseです。

cmd4はボタンが押されたらSet / 一定間隔でGetが発行させると、前述しました。
そのコマンドは、"state_cmd"に書かれたものになります。
引数としてnameを送りますが、それで管理するのはちょっと不安ですので、"state_cmd_suffix"としてチャンネル番号を付与して送るようにしました。前述のBraviaServerもその前提で書きました。
また、Getコマンドは上記のpollingで設定した間隔(秒)で送られます。timeoutはmsです。

この結果、cmd4は以下のようなcmdを実行します。
Set時(Button押したとき)
/home/pi/homebridge/tvChannels/channel.sh Get 'Eテレ' 'On' 021
Get時(20秒に一回)
/home/pi/homebridge/tvChannels/channel.sh Set 'Eテレ' 'On' true 021
引数の数が違うことに注意が必要です。

起動用スクリプト

最後にcmd4が叩きにくるスクリプトも用意します。
cmd4が直接BRAVIAサーバにリクエストする作りはちょっと知恵を使わないといけないので、Wrapper替わりになるシェルスクリプトを置きます。
なお、jqを使っていますので入ってなかったら入れておいて下さい。
curlも使ってますが、さすがに標準で入っていると思う。まあ念のため書いておく。

$ sudo apt-get install jq
$ sudo apt-get install curl 

以下の起動用スクリプトを
/home/pi/homebridge/tvChannels/bravia.sh(cmd4のコンフィグで指定した場所)
において下さい。

bravia.sh
#!/bin/bashif["$1"="Get"];then
  JSON=$(curl -s http://localhost:3000/channel/${4})result=$(echo${JSON} | jq  '.result')if[${result}=true];then
    echo 1
  else
    echo 0
  fi
   exit 0
fi

if["$1"="Set"];then
   if["$3"="On"];then
      if["$4"="true"];then
        JSON=$(curl -s-XPUT http://localhost:3000/channel/${5})echo 1
      fi
   fi
   exit 0
fi

exit 1

保存したら実行許可を付けておきましょう。

$ chmod 0755 /home/pi/homebridge/tvChannels/bravia.sh

【補足】
cmd4で叩かれたスクリプトは標準出力に結果を返す(echoする)必要があります。
返す結果はcmd4で作ったアクセサリの種類(操作するものの種類)によります。
スイッチの場合はOnかOffしかありませんので、どちらの状態なのかを返します。「On」という状態がtrueかfalseかを返す、と理解してください。
上記スクリプトでecho 1となっているものは、On状態がtrue。つまりOn。
echo 0となっているものはOn状態がfalse。つまりOffです。
Getコマンドでは現在のテレビの状態を調べて、ボタン自身のチャンネルと実際にテレビに映っているチャンネルが一致すればtrueを返すのでonになります。一致してなかったらfalseを返すのでoffになります。
その結果、20秒経ってGetコマンドが呼ばれた際に、OnのままになっているチャンネルがOffになり、実際のテレビの状態と一致します。

スイッチではなくたとえばボリュームを作った場合は echo 40のように操作後の値を返す必要があります。この場合は「ボリュームが40」と言う意味になります。

最後に

これで、Hey! siriとやりたかったことは実現出来ました。
紆余曲折しちゃったんですが、結局API叩いているので、最初に入れたhomebridge-braviaプラグイン無しで、テレビの電源入れるAPI叩くスイッチ用意すれば良いんじゃないの?とか、これならhomebridgeのプラグインを新しく作るのが良いんじゃないの?とかそんな気もします。

BRAVIAのAPIはいろんなことが出来るので、音量調整とかも可能ですし、リモコンをエミュレートすれば視聴中の番組録画なども出来そうです。

ぜひチャレンジしていただければな、と思ったりもします。


  1. BSはisdbsでなくisdb b s。BS放送とCS放送を区別するためにこう名付けたと思われます。 

  2. 北米ではATSCのはずです。テレビ実機からの取得方法が後述されていますので、実際の値はそちらで取得して下さい 


API Endpointのエントリポイント・コードの書き方

$
0
0

API Gatewayとかから飛んできたリクエストをLambdaでいい感じに処理する、よくあるバックエンドコード
この辺のいい感じの記述を模索してて最近こんな感じになっています。

最近の自分の書き方

/**
 * フォーマットに関してはこいつに任せる。
 */construn=async(event,hoehoe)=>{// どでかいtry-catchで例外をアプリ内に封じ込めるtry{// 悩むけどこの辺の情報はこっちでいいかなconstname=event.pathParameters.name;// 処理は極力別クラスに任せるconstservice=newService(hoehoe);constpet=service.getPet(name);// 仕様と対応するので正常系の出口は一箇所returngetSuccessResponse({color:pet.color.value,size:pet.size||99999,});}catch(error){console.error(error);// 最低限の整形をしてエラーを通知する。ここはアプリの最終防衛ラインなので重点的にテストreturngetErrorResponse(error);}}/**
 * エントリポイント
 * 外部プラットフォーム依存するコードを集約する
 * ここは結合テストのスコープ(LamndaやAPI Gatewayのテスト機構を使う)
 */exports.handler=async(event)=>{returnawaitrun(event,process.env.HOGEHOGE// 環境変数はスコープがでかいので入り口で捕まえて引き回す);};

Firebase Authentication 匿名ログイン 成果物URL:https://tokumei.netlify.app

$
0
0

成果物

https://tokumei.netlify.app

ワイ 「ログイン後Firebaseから寿命が1時間のJWTトークンが取得できる」
ワイ 「現実的な使い方をするなら」
ワイ 「JWTトークンを自分が使ってるオリジンサーバに送って改ざんされていないかチェックする」
ワイ 「ライブラリはfirebase-adminを使う」

トークンが改ざんされていないかチェックするソース

package.json
{"dependencies":{"firebase-admin":"^9.1.0"}}
index.js
constadmin=require('firebase-admin');// 各自用意constfirebaseConfig={type:'',project_id:'',private_key_id:'',private_key:'',client_email:'',client_id:'',auth_uri:'',token_uri:'',auth_provider_x509_cert_url:'',client_x509_cert_url:'',};admin.initializeApp({credential:admin.credential.cert(firebaseConfig),});constmain=asyncjwtToken=>{try{constuser=awaitadmin.auth().verifyIdToken(jwtToken);console.log('uid: '+user.uid);}catch(error){console.log(error);}};main('トークン');

フロントエンド ソースコード

package.json
{"scripts":{"start":"parcel src/index.html --out-dir public"},"dependencies":{"babel-polyfill":"^6.26.0","firebase":"^7.18.0","parcel-bundler":"^1.12.4","react":"^16.13.1","react-dom":"^16.13.1"}}
src/index.html
<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>匿名ログイン</title><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I"crossorigin="anonymous"/><linkrel="stylesheet"href="style.css"/></head><body><divid="app"></div><script
      src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/js/bootstrap.min.js"integrity="sha384-oesi62hOLfzrys4LxRF63OJCXdXDipiYWBnvTl9Y9/TRlw5xlKIEHpNyvvDShgf/"crossorigin="anonymous"></script><script src="index.js"></script></body></html>
src/index.js
importReactfrom'react';importReactDOMfrom'react-dom';import'babel-polyfill';import'firebase/auth';constfirebase=require('firebase/app');// 各自用意constfirebaseConfig={apiKey:''};firebase.initializeApp(firebaseConfig);constApplication=()=>{const[jwtToken,setJwtToken]=React.useState('null');const[uid,setUid]=React.useState('null');const[display,setDisplay]=React.useState(false);React.useEffect(()=>{firebase.auth().onAuthStateChanged(asyncdata=>{if(data!==null){setUid(data.uid);setJwtToken(awaitfirebase.auth().currentUser.getIdToken(true));}setDisplay(true);});},[]);if(!display){return(<><div><divclassName="position-absolute h-100 w-100 m-0 d-flex align-items-center justify-content-center"><divclassName="spinner-border text-primary"role="status"><spanclassName="sr-only">Loading...</span>
</div>
</div>
</div>
</>
);}return(<>{uid==='null'?(<buttontype="button"className="btn btn-primary"onClick={()=>{setDisplay(false);firebase.auth().signInAnonymously().then(async()=>'').catch(()=>{alert('ログイン失敗');});}}>ログイン</button>
):(<buttontype="button"className="btn btn-danger"onClick={()=>{firebase.auth().signOut().then(()=>{setDisplay(true);setUid('null');setJwtToken('null');alert('ログアウトしました');}).catch(()=>{alert('ログアウト失敗');});}}>ログアウト</button>
)}<br/><br/><tableclassName="table table-bordered"style={{width:'100%'}}><theadclassName="table-primary"><tr><th>uid</th>
<th>JwtToken</th>
</tr>
</thead>
<tbody><tr><td>{uid}</td>
<td>{jwtToken}</td>
</tr>
</tbody>
</table>
</>
);};ReactDOM.render(<Application/>,document.getElementById('app'));
src/style.css
table{width:100%;border-collapse:collapse;table-layout:fixed;}tabletd{overflow:hidden;word-break:break-all;}

コマンド

$ npm start

ワイのGitHubとか

GitHub: https://github.com/yuzuru2
YouTube: https://www.youtube.com/channel/UCuRrjmWcjASMgl5TqHS02AQ
Qiita: https://qiita.com/yuzuru2
LINE: https://line.me/ti/p/-GXpQkyXAm
Twitter: https://twitter.com/yuzuru_program
成果物まとめ: https://qiita.com/yuzuru2/items/b5a34ad07d38ab1e7378

最近の投稿

【第1回】「みんなのポートフォリオまとめサイト」を作ります~プロトタイプ作成編~

【第2回】「みんなのポートフォリオまとめサイト」を作ります~REST API編~

【第3回】「みんなのポートフォリオまとめサイト」を作ります~SNSログイン編~

【第4回】「みんなのポートフォリオまとめサイト」を作ります~フロントエンド編~

いいねが多い記事

Google Apps Scriptの神ライブラリ「cheeriogs」でヤフートップをスクレイピングする。GASのスクレイピングはもうこのライブラリ一択やな。

GAEでNode.jsを使ってHello Worldをするまで

$
0
0

GAEを使って、Node.jsのHello Worldを行った際の備忘録。

と言っても、Googleの公式ドキュメント通りに行えば大体大丈夫でした。
たまに詰まっても、ググれば出てきたって感じでした。

ではさっそく。

GAEとは

Google App Engine の略。
Google Cloud Platform の一部。
Google のサーバーを借りてwebアプリ等を公開できる。

公式ドキュメントを参考にやってみる

まずは、こちらの手順に従う。
スタンダード環境で Node.js を使用するためのクイックスタート
https://cloud.google.com/appengine/docs/standard/nodejs/quickstart#local-machine_1

以下、躓いたところの対処法を残しておく。

3.リージョンを選択
東京なら asia-northeast1 だ。
https://cloud.google.com/about/locations?hl=ja#locations

スクリーンショット_081920_084150_PM.jpg

5.Node.js環境準備
・node.js 環境を整える際に、NVM という、node.jsのバージョン管理コマンドをインストールする際に、node.jsをアンインストールする必要がある。(すでにnode.jsインストール済みの場合)
・npm install --save express をする前にnpm init をして、package.jsonを作る必要がある。
(これは、Node.jsをちゃんと理解していなかったからですね(^^;)

既存のNode.jsアンインストール
こちらを参考に。
https://funacchi.qrunch.io/entries/vsnfmdk3bYvnsUhn
の「3. Node.jsのアンインストール」

https://github.com/coreybutler/nvm-windows#uninstall-existing-node
の「Uninstall existing node」、「Uninstall existing npm」

NVMのインストール
こちらを参考に。
https://github.com/coreybutler/nvm-windows

最初どうすればいいか分からなかったが、
下記の手順でできた。
1.下記のサイトから使いたいVersionの「nvm-setup.zip」をダウンロード
(筆者の場合「1.1.7 - Maintenance Release」を選択した)
https://github.com/coreybutler/nvm-windows/releases
2.「nvm-setup.zip」を解凍。
3.「nvm-setup.exe」を起動。指示に従う。

※「nvm install xxx」や「npm install xxx」を実行すると、処理が完了しているのに途中で画面が止まることがあるから、その際は「Enter」を押すこと。(処理が止まるのは、僕のPCだけかな?)

あとは、「Hello World をローカルで実行する」に従うだけ。
リンクテキスト
Hello World をローカルで実行する

日をまたいでの作業になったため、一度CMDを閉じていた。
CMDを閉じると、もう一度 gcloud init を実行しないといけない。
参考:https://qiita.com/koshi_an/items/472659f89ef3e472b896

【niv】package.jsonのscripts一覧をターミナルから確認できるCLIを作ってみた

$
0
0

作ったもの

package.jsonに登録されている npm-scriptsをターミナルから確認、実行できるCLIを作ってみました。

説明

package.jsonがあるフォルダで

$ niv

と実行すると
スクリーンショット 2020-08-21 18.03.39.png
こんな感じで npm-scripts一覧が表示されます。
hjklまたは 矢印キーを動かし、スクリプトを選択できます。
エンターキーで選択しているスクリプトを実行します。
ctrl + cでキャンセルできます。

インストール

npmまたは yarnでインストールできます。

npm install-g niv
# or
yarn global add niv

モチベーションとか技術のお話

いろんなプロジェクトに参加しててそれぞれ npm-scriptsがバラバラなので一覧にしてさらに実行もできるCLIがほしかったので作りました。

使った技術セットはこんなかんじです。

技術説明
TypeScript型システムがついたJavaScript。最近V4が出たらしい...?
oclifNode.jsのCLIフレームワーク。TypeScriptにも対応してて簡単にCLIが作れちゃう。

さいごに

ちょい実行速度が遅いところもありますが、よければ使ってみてください〜
最後まで読んでいただきありがとうございました〜

EC2でReactをhttpsでnpm startして外部からアクセスする

$
0
0

EC2上でReactアプリをhttpsでホストすることがあったので備忘録です。

AWSならACMとかELBとかRoute53とか使えるわけですが、
ちょこっとテスト的に動かしてみたいだけなのでできるだけ簡単にSSL化したいと思います。

結論から言うと
HTTPS=trueにしたnpm startでlocalhostでhttpsサーバーを立てて、
EC2外部からのアクセスはnginxでlocalhostにリダイレクトします。

環境

  • EC2 (Amazon Linux 2)
  • React
  • nginx
  • Node.js

1. EC2を立てる

EC2立てます。
http, httpsのポートをあけておきます。

2. nginxインストール・設定

インストール

nginxインストールしますが、Amazon Linux 2ではnginxがyumでサポートされていないらしいので
以下のコマンドでインストールします。

$ sudo amazon-linux-extras install nginx1.12

SSL化設定

SSL化するにあたって証明書を作ります。

$ cd ~ 
$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 365 -req -signkey server.key < server.csr > server.crt
$ sudo mv server.* /etc/nginx/conf.d/
$ cd /etc/nginx/conf.d
$ sudo chown root:root server.*

nginx用の設定ファイルを新規に作成して、
localhostへのリダイレクトを設定します。

$ cd /etc/nginx/conf.d
$ touch ssl_redirect.conf

新規に作成したファイルに以下の内容を記述します。

ssl_redirect.conf
server {
    listen       443 ssl;
    server_name  localhost;
    ssl_certificate      /etc/nginx/conf.d/server.crt;
    ssl_certificate_key  /etc/nginx/conf.d/server.key;
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

これでnginxの設定が完了です。
nginxを起動、または再起動します。

$ sudo service nginx start
または $ sudo nginx -s reload

3. Node.js, Gitのインストール

$ sudo yum -y install git
$ vim ~/.ssh/id_rsa (gitクローン用に鍵の内容をコピーしときます)
$ chmod 400 ~/.ssh/id_rsa
$ git clone https://github.com/~
node.jsインストール
$ sudo su - root
$ curl -sL https://rpm.nodesource.com/setup_current.x | bash -
$ yum install -y nodejs

4. npm startでhttpsのlocalhostサーバをたてる

npm install
HTTPS=true npm start

以上で設定完了です。
EC2の外部からEC2のパブリックIPにアクセスすると、
EC2のlocalhostにリダイレクトされてhttpsでホストされたReactアプリの確認ができます。

Node.jsでディレクトリ配下にあるファイルを再帰的に探索する

$
0
0

下記のディレクトリ構造から、txtファイルのpathとfile名を一覧で取得する方法(Node.js)です。

./dir
├── dir2
│   ├── dir3
│   │   ├── file2.txt
│   │   └── file3.md
│   ├── file2.txt
│   └── file3.md
├── file1.md
└── file1.txt

実装コード

constfs=require('fs');constpath=require('path');constsearchFiles=(dirPath)=>{constallDirents=fs.readdirSync(dirPath,{withFileTypes:true});constfiles=[];for(constdirentofallDirents){if(dirent.isDirectory()){constfp=path.join(dirPath,dirent.name);files.push(searchFiles(fp));}elseif(dirent.isFile()&&['.txt'].includes(path.extname(dirent.name))){files.push({dir:path.join(dirPath,dirent.name),name:dirent.name,});}}returnfiles.flat();};constdirPath='./dir';console.log(searchFiles(dirPath));

出力内容

[
  { dir: 'dir/dir2/dir3/file2.txt', name: 'file2.txt' },
  { dir: 'dir/dir2/file2.txt', name: 'file2.txt' },
  { dir: 'dir/file1.txt', name: 'file1.txt' }
]

nowコマンドが使えなくなっていたのでnodeをアップデートした

$
0
0

ある日、Zeit Nowのnowコマンドを使おうとしたところ、実行できなくなっていました。
1ヶ月前くらいまでは使えていたのに...
解決方法をチップスとして残します。

Nowとは

nowと打つだけでデプロイできてしまうシンプルで簡単に使えるPaaSです。
https://vercel.com/home
私はNext.jsで利用していますが、他のNode.jsのフロントエンドFWやPythonやGoなどでも使えるようです。

Next.js + nowの使い方はこちらが参考になります。
https://qiita.com/shioiyan/items/540a0d68118c0384f4e4

nowコマンドが実行できなくなった

now
> UPDATE AVAILABLE Run `npm i -g now@latest` to install Now CLI 19.2.0
> Changelog: https://github.com/zeit/now/releases/tag/now@19.2.0
> Error! The specified token is not valid

一度設定済みなのでnowでデプロイできるはずなのですが、Errorになりました。
アップデートで古いバージョンだと使えない仕様になったのでしょうか。

解決方法

npm i -g now@latestせよといわれているので、おとなしくコマンドを実行します。

npm i -g now@latest
> Error! Detected unsupported Node.js version.
> Expected ">= 10" but found "8.11.3".
> Please update to the latest Node.js LTS version to install Vercel CLI.
> npm WARN notsup Unsupported engine for now@19.2.0: wanted: {"node":">= 10"} (current: {"node":"8.11.3","npm":"6.13.6"})

今度はnodeのバージョンが低すぎると怒られました。
というわけでnodeをアップデートします。

// nコマンドのインストール
npm install -g n

// 安定板のインストール
n stable

// バージョン確認
node -v
> v12.18.3

これで無事にnowコマンドが使えるようになりました。

now
> Error! The specified token is not valid

上記のように怒られたときは、

now loginでメールアドレスを認証させて、
再びnowコマンドを実行すればデプロイできるようになります。

ちなみに--prodをつけるだけで設定なしで本番環境にdeployできるので便利です。

// DEV
now

// 本番
now --prod

以上nowのチップスでした。

参考にさせてもらいました。
node.js アップデート手順 https://mebee.info/2019/10/12/post-1793/#outline__3


Angular開発環境をDockerコンテナで立てる

$
0
0

Angular開発環境をDockerコンテナで立てる

Angular 開発環境を Docker コンテナで立てる際に、コンテナ内の localhost:4200 にホストマシンからアクセスできないなど手こずったので、環境構築手順を記載したい。

目次

導入

この記事で構築する環境は以下の構成になる。

Angular の Docker コンテナを立てるところまでは普通なのだが、これをホストマシンのブラウザからアクセスするために Node.js でリバースプロキシをコンテナ内部に立てる部分の情報が意外とネットに少なかった。Node.js で外部ライブラリを使わずにリバースプロキシを立てる際、Node.js: 外部パッケージを使わずに14行で作るHTTPリバースプロキシが非常に参考になった。ありがとうございます!

ホストマシンのブラウザからアクセスできない問題の解決法としては、例えば、DockerでAngular quick startには、.angular-cli.json を作成することで対処できるとあったが、手元の環境ではうまく機能しなかったため、リバースプロキシを用いる方法を採った。

大まかな流れは以下の通り。

  • 構成ファイルをダウンロード
  • docker-compose up -d を実行
  • docker container exec コマンドで Angular のプロジェクトを作成と起動

これでホストマシンから Angular のインストール完了画面を見ることができるようになる。楽ちんだ。

作業フロー

中身の詳細はさておき、最短で構築する方法をコマンドとももに記載したい。

構成ファイルをダウンロード

以下の構成ファイルがある。

  • docker-compose.yml
  • Dockerfile
  • proxy.js
  • package.json

https://github.com/freopeから、これらのファイルを作業用ディレクトリにダウンロードする。

git clone https://github.com/freope/angular-on-docker

docker-compose コマンドの実行

ダウンロードした angular-on-docker ディレクトリに移動し、コンテナを立ち上げて、

# angular-on-docker ディレクトリに移動cd angular-on-docker

# コンテナの立ち上げ
docker-compose up -d#=> Creating angular_angular_1 ... don

立ち上がったコンテナの名前を見る。

# コンテナの名前を見る
docker container ls#=> CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES#=> a0f2d5198c07        angular_angular     "docker-entrypoint.s…"   21 minutes ago      Up 21 minutes       0.0.0.0:4200->3000/tcp   angular_angular_1

出力の一番右側の NAMES カラムをみると、angular_angular_1 という名前のコンテナが立ち上がっているのが分かる。

Angular プロジェクトを作成

コンテナを立ち上げると、カレントディレクトリに angular という名前のディレクトリが現れるが、これはコンテナ内の ~/project という作業ディレクトリとマウントされている。

このディレクトリに Angular プロジェクトを作成する。

# Angular プロジェクト sample を作成
docker container exec-it angular_angular_1 ng new sample

実行すると、

  • ? Would you like to add Angular routing? Yes
  • ? Which stylesheet format would you like to use? CSS

と聞かれるので、適当に答えてエンター。

Angular 開発用サーバを起動

# プロジェクトをコンパイルし、Angular の開発用サーバをコンテナ内に起動
docker container exec-it-w /project/sample angular_angular_1 ng serve

# バックグランドで実行する場合は -d オプションを付ける# docker container exec -d -it -w /project/sample angular_angular_1 ng serve

ホストマシンのブラウザから localhost:4200 にアクセスし、以下のような画面が表示されればOK.

各構成ファイルの役割

docker-compose.yml

docker-compose.yml
version:"3"services:angular:build:.ports:-"4200:3000"volumes:-./angular:/project

ports の箇所で、ホストマシンの 4200 番ポートを、コンテナ内の 3000 番ポートにポートフォワード。

volumes の箇所で、マウントされる angular ディレクトリの場所を、指定している。

Dockerfile

FROM node# TypeScript をインストールRUN npm install-g typescript

# Angular をインストールRUN npm install-g @angular/cli

# リバースプロキシのプログラムをコンテナにコピーCOPY proxy.js /home/node/COPY package.json /home/node/# コンテナ内での作業ディレクトリを変更WORKDIR /project# デフォルトでリバースプロキシサーバーを立ち上げるCMD node /home/node/proxy.js

最終行で、docker container run コマンド実行時に、コンテナ内で実行されるコマンドを指定しなかった場合、Node.js のプロキシサーバ proxy.js が立ち上がる設定をしている。

proxy.js

/**
 * Reverse Proxy
 * [Node.js: 外部パッケージを使わずに14行で作るHTTPリバースプロキシ](https://qiita.com/suin/items/0ca6d44c7671abdc032b)
 */import{createServer,request}from'http'createServer((clientReq,clientRes)=>{constserverReq=request({host:'127.0.0.1',port:4200,method:clientReq.method,path:clientReq.url,headers:clientReq.headers,}).on('error',()=>clientRes.writeHead(502).end()).on('timeout',()=>clientRes.writeHead(504).end()).on('response',(serverRes)=>{clientRes.writeHead(serverRes.statusCode,serverRes.headers)serverRes.pipe(clientRes)})clientReq.pipe(serverReq)}).listen(3000)

プロキシサーバのプログラム。

Angularの開発用サーバーが localhost:4200 で立ち上がるので、この 4200 番ポートを、コンテナ外部に開いている 3000 番ポートにポートフォワードする。

docker-compose.yml で、ホストマシンの 4200 番ポートを コンテナの 3000 番ポートにポートフォワードしていたので、ホストマシンの localhost:4200 で Angularの開発用サーバーにアクセスできるようになる。

ベースのプログラムを作ってくれた方、ありがとうございます!

package.json

{"type":"module"}

proxy.js で import {createServer, request} from ‘http’ を実行できるようにするための設定。

参考資料

【JS】Socket.io を使ってリアルタイムな通信を実現する

$
0
0

Socket.io を使って、ライブコーディングもどきを作った時のメモです

Get started | Socket.IO
Node.jsからSocket.IOを使うための事前知識 - Qiita

Socket.io

リアルタイムで双方向の通信を可能にする技術です。

WebSocketが他のHTTPベースのプロトコルと異なるのはステートフルな通信であるという点です。
HTTP(Ajax,Comet)では通信のたびに新たにコネクションを確立する必要がありますが、WebSocketではその必要がありません。
また、HTTPより軽量なヘッダを扱うことから、通信コストが低い利点もあり、よりリアルタイム性の高いプロトコルであることがWebSocketの特徴です。
Node.js + Express + Socket.ioで簡易チャットを作ってみる - Qiitaより引用

インストール

npm install socket.io

GitHub - codemirror/CodeMirror: In-browser code editor

実装メモ(一部抜粋)

constserver=http.createServer(app);// socket.io を定義したファイル指定require('../socket-server')(server);// 接続先設定constsocketIO=require('socket.io');constsocket=io.connect('http://localhost:3000');io.on('connection',function(socket){socket.on('message',function(data){io.emit('message',data);// データを送信})})

重要メソッド

socketio.on

socketio.on(<イベント名>, callback)はイベントを検知します。
つまりデータを受信することが可能になります。

socketio.emit

socketio.emit(<イベント名>, data)でイベントを発火します。
つまり、データの送信を可能にします。

socket.join(socket.room)

特定の roomに join させるには、 joinを使用します。

socket.leave

joinの逆です。
socket.leave(socket.room)

io.to(socket.room).emit

io.to(socket.room).emit('event_name', data)
特定の roomに所属するクライアントにのみイベントを送信することが可能になります。
一位のID などを持たせたりすれば、特定の roomに入っているユーザーじ対して、何かしらのアクションを行うことが可能になります。

Socket.ioでチャットルームを実現する方法 -- ぺけみさお

ライブコーディング周りメモ

インストール

npm install -S git+https://github.com/Operational-Transformation/ot.js.git

実装

下記の ot.jsを追加する
GitHub - Operational-Transformation/ot.js

constot=require('ot');constsocketIOServer=newot.EditorSocketIOServer(str,[],data,function(socket,cb){cb(true);});socket.on('doc',function(obj){editor.setValue(str);cmClient=window.cmClient=newEditorClient(obj.str,obj.revision,obj.clients,newSocketIOAdapter(socket));})

OBS をスマホや M5GO(M5Stack)から遠隔制御 〜 MQTT や obs-websocket を利用 〜

$
0
0

はじめに

この記事は、ライブ配信などで使われる OBS Studio(以下、OBS と記載)を遠隔制御してみたという内容です。

まずは、その仕組みを実際に動作させている様子をご覧ください。


この動画の内容について補足すると、PC上で OBS が動いており、その OBS では以下の 3つのシーンが設定されています。
  1. 「休憩中」という文字を表示するシーン
  2. PC のカメラ映像を画面いっぱいに表示するシーン
  3. スライドを画面いっぱいに表示しつつ PC のカメラ映像も同時に表示しているシーン

上記の 3つのシーンの切り替え操作を、スマホや M5GO(M5Stack)といた別デバイスから制御しています。全体の構成は以下のとおりです。
全体の構成図.jpg

以下で、その仕組みの詳細について書いていきます。

利用している仕組み

ここでは、OBS を遠隔制御するために用いた以下のプラグインやパッケージ・サービスについて記載します。

  • obs-websocket(プラグイン)
  • obs-websocket-js
  • MQTT.js
  • shiftr(MQTTブローカー)
  • UIFlow

また、今回用いた仕組みの話ではないのですが、MQTT の開発に便利に使えるアプリ(MQTT.fx)についても少し記載しています。

今回の仕組みで用いたプログラムの詳細に関しては、この後の「プログラム」の項目でまとめて記載します。

OBS関連

obs-websocket(プラグイン)

今回、OBS を外部から制御する一番のポイントは obs-websocketというプラグインです。

●obs-websocket - Remote-control OBS Studio from WebSockets | OBS Forums
 https://obsproject.com/forum/resources/obs-websocket-remote-control-obs-studio-from-websockets.466/
●Palakis/obs-websocket: Remote-control OBS Studio through WebSockets
 https://github.com/Palakis/obs-websocket
obs-websocket_-_Remote-control_OBS_Studio_from_WebSockets_OBS_Forums.jpg
このプラグインを OBS に追加してやることで、外部のプログラムやアプリから OBS を制御することができるようになります(自分が見つけたきっかけは、MQTT や WebSocket を使ったライブラリ等を、GitHub上でキーワード検索を行ったことでした)。

プラグインの名称にあるとおり、OBS の制御には WebSocketを用いる形です。
以下の提供元のページに書かれたプロトコルに沿って WebSocket を用いた通信を行うことで、冒頭に紹介したシーンの切り替えだけでなく、様々な OBS の操作を外部のプログラムやアプリから行うことができます。

●obs-websocket/protocol.md at 4.x-current · Palakis/obs-websocket
 https://github.com/Palakis/obs-websocket/blob/4.x-current/docs/generated/protocol.md

WebSocket で通信させることができれば、OBS が動いている PC内の別プログラム・アプリからでも制御できますし、PC の外の別デバイスから制御することも可能です。

obs-websocket のセットアップ

このプラグインを使うための手順はシンプルで、プラグインのインストールを行ってしまえば OBS を起動した際に WebSocketを利用するためのサーバが連動して立ち上がります。
なお、ポート番号やパスワード認証といったプラグインに関わる設定を変更することもでき、具体的には OBS を起動後のメニューの中から「ツール ⇒ Web Server Settings」とたどった先で変更が可能です(自分は、PC内の別のプログラムがこのプラグインのデフォルト設定のポート番号を利用していたため、競合しないようにデフォルトのポート番号ではないものに変更し、その話とは別になりますが、認証の設定の有効化も行いました)。

obs-websocket のクライアントのサンプル

obs-websocket のページに、以下のような記載がありました。
obs-websocket_Client.jpg
どうやら、ブラウザ上で動作するクライアントが、以下のリンクから簡単に試せるようです。
●OBS Tablet Remote
 http://t2t2.github.io/obs-tablet-remote/
OBS_Tablet_Remote.jpg

obs-websocket-js

上記の obs-websocket のプロトコルに合わせた通信部分の処理を書いてやれば、外部プログラム・アプリからの制御はできるようなのですが、まずは手軽に試せればと思い obs-websocket-jsを使いました。

これは、obs-websocket の GitHub のページ内(以下の画像の部分)で紹介されていたもので、obs-websocket の通信部分をより簡単に書けるようにしてくれるもののようでした。
obs-websocket_APIs.jpg
様々な言語向けのものが(様々な方によって)準備されているようですが、自分はこの画像内のリストで一番上に書かれた「Javascript (browser & nodejs): obs-websocket-js by Brendan Hagan」を使いました(選定に関しては、比較評価をして選択したのではなく、単に自分が Node.js・JavaScript を使い慣れているという理由で選択)。

obs-websocket-jsのページには以下のサンプルが書かれており、これを参考にすると「認証+接続の処理」や「設定したシーンの一覧の取得」・「シーンの切り替えの指定」を用いる処理はすぐに実装できそうです。
obs-websocket-js_Sample.jpg
実際に、このサンプルの一部を自分の obs-websocket のプラグイン設定に合わせた形で書きかえ(※ OBS側で自分がデフォルトから変更したポート番号と、設定した認証用のパスワードの部分を書きかえ)、プログラムを実行してみて OBS の外部からシーンの切り替え等が行えることを確認できました。

この後、keypressを組み合わせて、PC の特定のキーを押したら OBS のシーンが押下したキーと対応づけられたシーンに切り替わる、というものをお試しで実装したのですが、今回の本題と少しズレる部分であるため詳細については省略します。

MQTT関連

obs-websocketプラグインと obs-websocket-js(または、JavaScript以外の言語向けのもの)を使うことで、obs-websocket のプロトコルの詳細まで踏み込むことなく OBS を制御をすることが可能になりました。
ここまで試せたところで、自分がこの OBSプラグインを見つけた際にやりたいと思った「OBS の操作を PC の外の別デバイスから行う」という内容に着手することにしました。

やり方の 1つとして、PC上で動かした Node.js のプログラム(obs-websocket-js を用いたプログラム)に Webサーバの処理・Webページ用のファイルなどを追加して操作用の Webページを用意し、そこへスマホでアクセスして操作する方法がありそうです。
ただ、スマホ以外のマイコンモジュール等とも連携させたいと思い、さらに PC側の実装はそれらの端末用に別で用意するのではなく共通化できれば、と思いました。そこで、マイコンモジュール等を含めたデバイス間連携で最近よく使っている MQTT を組み合わせてみることにしました。

MQTT.js と shiftr(MQTTブローカー)

「はじめに」の項目で掲載した図の構成のとおり、PC と他デバイスの間の通信に MQTT を用いることとし、PC上の Node.js のプログラムで MQTT.jsを用いた処理を追加しました(※ 構成図のスマホの部分の JavaScript実装でも MQTT.js を利用しています)。
MQTT の通信を仲介する MQTTブローカー に関しては、自分が今回使う用途はローカルネットワーク内での通信が行えれば十分であったため、OBS が動いている PC上で動かす形でも良かったのですが(※ 例えば Mosquittoを使う等)、過去に作成したプログラムで外部の MQTTブローカーを提供するサービスの 1つである「shiftr」を使うものが手持ちであったので、shiftr を使う構成にしました。

ここでは、MQTT・MQTT.js・shiftr の詳細な説明は割愛しますが、以下に参考となりそうな記事のリンクを記載します。

【余談】 MQTT.fx(GUIベースの MQTTクライアントアプリ)

これは、利用した仕組みではなく、開発の途中で用いたアプリの話になるのですが、便利に使えるものなので紹介します。

冒頭で紹介した今回の構成で、MQTT のメッセージを受信側の PC上の処理を実装した時点で、MQTT の処理部分を軽くテストするために MQTT.fx というアプリを使いました(※ このアプリが冒頭の構成図の左にある、2つのデバイスの役割を代替する形)。
MQTTfx.jpg
このアプリは Javaベースで作られた MQTTクライアントで、 MQTTブローカーやトピック・メッセージの設定などを GUI上で行うことができ、MQTT のメッセージ送受信(Publish/Subscribe)の操作・受信内容の確認等も同じく GUI上で行えて便利です。
マルチプラットフォーム対応であるため、普段は Mac をメインで使いつつ Windows も併用している自分にとって、どちらの環境でも使えるのもありがたい点です。

UIFlow

冒頭の動画の前半で使っていたボタン付のデバイス M5GO(M5Stack) のプログラムは、M5Stack用のビジュアルプログラミング環境である「UIFlow」を使いました(※ 以前、UIFlow を使って MQTT を用いるプログラムを作成したことがあったため)。
UIFlow は、以下の画像にあるようなブロック型のビジュアルプログラミングでプログラムを作成できます。
UIFlow.jpg
デバイスに内蔵された画面・ボタン等などの処理や、外付けできるセンサー等の専用部品(ユニット)に関する処理なども使えますが、さらに HTTPリクエストや MQTT といった、IoT に役立つ処理もビジュアルプログラミングで作成することができます。

利用するためには、デバイスでのセットアップやブラウザ上(UIFlow上)での端末に紐付いた API Key の指定などが必要ですが、今回の記事では割愛します。詳細は、公式ドキュメントや Web上の記事などをご覧ください。

obs-websocket(プラグイン)を使った事例

今回の仕組みを作る中で、この obs-websocket を使っている事例を軽く検索してみました。
ひとまず日本語に限定した検索結果にして、見つかった記事をいくつかざっと見てみました。

今回の記事に出てくる obs-websocket-js やシーンの切り替えの話以外に、M5Stack用のプログラム(C言語での実装)に obs-websocket との WebSocket での通信を実装した例や、Pythonライブラリを使った話、OBS上で YouTubeライブチャット欄を表示する設定(URL指定)を自動で更新する話などがありました。

これらを見ていて、シーン切り替え以外のことをやっている事例が混じっていて、様々な OBS の操作や設定変更を外部から制御する話を、もっと調べたり試してみたくなりました。

プログラム

ここでは、以下の図の赤矢印で示した 2箇所のプログラムについて記載します。
構成図で説明する部分.jpg
スマホの部分はシンプルな内容でもあるため、今回は割愛します。
おおまかには、HTML で 3つのボタンを表示させるようにし、JavaScript でそれらのボタンがクリックされた時に 押されたボタンによって異なる MQTT のメッセージが送信されるように実装しています(※ 他に MQTT関連の処理で、MQTT.js を読み込んで MQTTブローカーとやりとりするための設定を行っています)。

PC上の Node.js のプログラム

今回の切り替え操作の対象となる 3つのシーンは、OBS上で以下のように設定しており、これらを Node.js のプログラムから指定して切り替えることになります。
OBS_シーン.jpg

ここで記載するプログラムでは、2つのパッケージ(obs-websocket-js・mqtt)を利用します。
そのために、npmコマンドで obs-websocket-jsmqttの 2つのパッケージをインストールしてください。

npm i obs-websocket-js mqtt

プログラムの具体的な内容は、以下のとおりです。
プログラム中に 【 】 で囲まれた部分が複数ありますが、ご自身の環境に合わせて書きかえをしてください。

  • 書きかえが必要な部分のリスト
    • obs-websocket のポート番号
    • obs-websocket のパスワード(※ 認証を有効にした場合に利用する)
    • shiftr の ID
    • shiftr の PASS
    • MQTTクライアントのクライアントID
    • MQTT のトピック
    • MQTT でやりとりする文字列(※ 今回は 3つのシーンを用意したため、3種類)
constOBSWebSocket=require('obs-websocket-js');constmqtt=require('mqtt');constobs=newOBSWebSocket();obs.connect({// obs-websocket に関わる設定address:'127.0.0.1:【ポート番号】',// 同じPC内で接続、ポート番号のデフォルトは「4444」password:'【パスワード】'// 認証を有効にした場合に必要}).then(()=>{console.log(`Success! We're connected & authenticated.`);returnobs.send('GetSceneList');}).then(data=>{// 取得したシーンのリストを出力(サンプルそのまま)console.log(`${data.scenes.length} Available Scenes!`);data.scenes.forEach(scene=>{console.log(`${scene.name}`);});// OBSのシーン情報を取得できた後に、MQTT関連の設定を行うconstclient=mqtt.connect('mqtt://【shiftrのID】:【shiftrのPASS】@broker.shiftr.io',{clientId:'【MQTTのクライアントID(文字列を設定する)】'});client.on('connect',function(){console.log('MQTT: client has connected!');client.subscribe('【MQTTのトピックを設定する】');});client.on('message',function(topic,message){console.log('new message:',topic,message.toString());// MQTT で受信したメッセージの内容によって切り替えるシーンの指定を変えるswitch(message.toString()){case'【MQTTでやりとりする文字列1(シーン1つ目に対応)】':obs.send('SetCurrentScene',{'scene-name':data.scenes[0].name});break;case'【MQTTでやりとりする文字列1(シーン2つ目に対応)】':obs.send('SetCurrentScene',{'scene-name':data.scenes[1].name});break;case'【MQTTでやりとりする文字列1(シーン3つ目に対応)】':obs.send('SetCurrentScene',{'scene-name':data.scenes[2].name});break;}});}).catch(err=>{console.log(err);});obs.on('SwitchScenes',data=>{console.log(`New Active Scene: ${data.sceneName}`);});// You must add this handler to avoid uncaught exceptions.obs.on('error',err=>{console.error('socket error:',err);});

今回、元のサンプルをできるだけ流用する形にしたため、3つのシーンの名前を取得する処理の後にシーンの切り替え処理を実装し、そこで指定するシーンの名前を data.scenes[0].nameなどという形で書いています。
この部分は、わざわざプログラムで OBS からシーン名を取得するのではなく、それら 3つの名前をプログラム上に直接書いて利用する形でも良いかもしれません。

なお、こちらのプログラムを動作させて使う際は、プラグイン(obs-websocket)をインストール済みの OBS を先に立ち上げておいてください。

UIFlow のプログラム

M5GO(M5Stack)用に作成したプログラムは以下の画像のとおりです。

画像中に 【 】 で囲まれた部分が複数ありますが、ご自身の環境に合わせて書きかえてください。
なお、説明の都合上、文字列を記載する箇所に日本語の文字を書いていますが、UIFlow で日本語の文字は利用できないため、半角英数字・半角記号のみを書くようにしてください。

書きかえが必要な部分は以下にまとめました。この中の「MQTT のトピック」と「MQTT でやりとりする文字列(※ 3種類)」は、Node.js のプログラムで指定したものと同じものにする必要があります。

  • 書きかえが必要な部分のリスト
    • MQTTクライアントのクライアントID
    • shiftr の ID(※ 画像では見きれていますが、上から2つ目のブロックの右のほうの「ユーザー名」のところで指定できます)
    • shiftr の PASS(※ 画像では見きれていますが、上から2つ目のブロックの右のほうの「パスワード」のところで指定できます)
    • MQTT のトピック
    • MQTT でやりとりする文字列(※ 今回は 3つのシーンを用意したため、3種類)

UIFlowのプログラム.jpg

プログラムを M5GO(M5Stack)に書き込んだら、あとは OBS と Node.js のプログラムを動作させた状態で、M5GO(M5Stack)の3つのボタンを押すだけです。3つのボタンの何れかを押すと、OBS の表示が押したボタンに対応したシーンへと切り替わります。

おわりに

今回、OBS のプラグイン「obs-websocket」と MQTT などを組み合わせて、スマホや M5GO(M5Stack)から OBS を遠隔で制御するという内容を試しました。

今回試している制御内容は、自分が一番多く使いそうだったシーンの切り替え操作だけでしたが、本文中に書いたようにこのプラグインで制御可能な操作は他にも様々あるようです。OBS の利用を便利にしてくれそうな仕組みを他にも作れそうなので、シーン切り替え以外の制御などを引き続き調べて試してみようと思います。

生のNode.jsでプロジェクト作成時にやること

$
0
0

当然ではありますが、生のNode.js(特にWebアプリ)でプロジェクト作成時にやることが多く、HTTP通信に関する勉強にもなったのでメモを残しておきます。
Node.jsに関してはほぼ初心者なので、不足や誤りがあるかもしれません。発見した場合はコメントなどでご報告いただけますと幸いです。

プロジェクトの初期化

とりあえず初期化。
いろいろ聞かれるので、それぞれのプロジェクトに合った選択をしていきます。

$ npm init
  • package name: (フォルダ名) プロジェクトの名前を定義します。特に変更がなければそのままReturnまたはEnterでOK。
  • version: (1.0.0) バージョンを定義します。これも1.0.0のままでよければそのままReturnまたはEnterでOK。
  • description: プロジェクトの概要を定義します。これも空白のままでよければそのままReturnまたはEnterでOK。
  • entry point: (index.js) アプリケーションを起動するためのファイルを指定します。index.jsやmain.jsが多そう?これもindex.jsのままでよければそのままReturnまたはEnterでOK。
  • test command: テストコマンドの指定をします。これも空白のままでよければそのままReturnまたはEnterでOK。
  • git repository: Gitのリポジトリを指定します。これも空白のままでよければそのままReturnまたはEnterでOK。
  • keywords: npmに公開した時のキーワードを設定します。今回はWebアプリを作ることを想定しているのでそのままReturnまたはEnterでOK。
  • author: 製作者の名前を設定します。これもnpmに公開した時に使われるようなので、そのままReturnまたはEnterでOK。
  • license: (ISC) npm に公開された時のライセンスを記述します。これもnpmに公開した時に使われるようなので、そのままReturnまたはEnterでOK。

-yを末尾につけることで、すべてデフォルトのまま初期化することができます。

http-status-codesをインストールする

$ npm install http-status-codes --save#--saveはNode.js 5.0.0以降は自動で付与されるのでいらない

サーバーがリクエストの受信に成功したことを表す200や、Not foundを表す404などを返します。

リクエストとレスポンスに最低限必要なコードを書く

ポート番号の設定

index.js
constport=3000;//今回は3000番ポートを使用。 

httpモジュールのロード

index.js
consthttp=require('http');

http-status-codesモジュールのロード

index.js
consthttp-status-codes=require('http-status-codes');

サーバーを作成する

index.js
app=http.createServer((req,res)=>{res.writeHead(httpStatus.OK,{'Content-Type':'text/html'});res.write('<h1>Hello, World!</h1>');res.end();});

httpStatus.OKには200が入っています。
これをres.writeHeadの引数にとることで、リクエストが成功したことを表します。
その後、res.write()でボディを記述し、res.end()でレスポンスを閉じることで、クライアントにレスポンスを送信します。

ポートの監視

このコードの初めに記述した3000番ポートを監視します。

index.js
app.listen(port);

これでlocalhost:3000にアクセスすることができるようになりました。
実際にブラウザからアクセスしてみると、下の写真のようになっています。
image.png

まとめ

これで、サーバーを立てることができるようになりました。
実際にExpressなどのフレームワークなどを一切使わずにNode.jsをサーバーサイドとして使用する機会はあまり多くはないと思いますが、通信の仕組みをやんわりと理解することにも役立ちそうな気がします。

node.jsでnpm run devがエラーになった場合の対処法

$
0
0

nuxt.jsの開発環境でで以下を実行したらエラーが出ました。

$ npm run dev
:
This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?
:

結論

以下のコマンドを実行したら解決しました。一度消去後に再インストールしてます。

$ rm -rf node_modules
$ rm package-lock.json yarn.lock
$ npm cache clear --force
$ npm install

コマンド実行前もインストールされているはずでしたが、バージョンの違いでエラーが出る場合があるので一度消去して、再インストールしたらいけました!バージョンは変化していませんが、node?のバージョンが変化してのでしょうか。
詳しい方いたらご指摘いただけると幸いです。

$ npm -v
6.14.7
// 上記のコード実行後
$ npm -v
6.14.7

pkgでインストールしたnode.jsを、Nodebrewのnode.jsに入れ替えたい!

$
0
0

nodeのバージョン管理を簡単にした〜い

ので、個人的にnodeをインストールするときは最初からNodebrewを使っているのですが、仕事で使っているマシンでは何を思っていたのかpkgでインストールしていたので、入れ替えした手順を備忘として残したいと思います

同部署の人がnodeのバージョン変更で手こずってて、Nodebrewで一発やんけと嘲笑していたところ
自分もpkg版だったのでしれっと入れ替えた

とりあえず最初に、そもそもnode.jsが入っているかを確認しておきましょう

$ node -v

そしてNodebrewとは、👇

hokaccha / nodebrew

Node.js version manager. Contribute to hokaccha/nodebrew deve github.com


pkg(インストーラ)でインストールしたnode.jsをアンインストールする

まずは、すでにインストーラでインストールされているnodeをアンインストールします
ターミナルで下記を実行します

$ lsbom -f -l -s -pf /var/db/receipts/org.nodejs.node.pkg.bom \

そして下記を1行ずつ実行します

| while read i; do

 sudo rm /usr/local/${i}

done

sudo rm -rf /usr/local/lib/node \

/usr/local/lib/node_modules \

/var/db/receipts/org.nodejs.*

「\」が最後についているコマンドを実行すると、次の行の初めに「>」がつきますが、そのままコマンド実行して大丈夫です

一緒にnpmもアンインストールします

$ sudo rm -rf ~/.npm

上記コマンドまでで、インストーラでインストールしたnode.jsとnpmのアンインストールが済みました🎉

Nodebrewをインストールする

Nodebrewを入れる前に…

Homebrewをインストールしていないと、Nodebrewもインストールできません
(HomebrewでNodebrewをインストールするので…)

なので、Homebrewがインストールされているか確認しましょう

$ brew doctor

⚠️エラーが出ていたら、エラー内容を確認して解消しておきます

Your system is ready to brew.

問題なくインストールされていれば、「(Homebrewの)準備できてるよ!」と言われます

インストール自体をまだしていなければ、下記ワンライナーを実行して、インストールしておきましょう

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
Homebrew

Homebrew The Missing Package Manager for macOS (or Linux).   

Nodebrewをインストールする

インストーラでインストールしたnode.jsをアンインストールして、Homebrewもインストールしたところで、
やっとこさNodebrewをインストールします!

$ brew install nodebrew

Homebrewのアップデートがある場合は、ここでしれっとアップデートされます😂

~~~中略~~~

==> Downloading https://github.com/hokaccha/nodebrew/archive/v1.0.1.tar.gz
==> Downloading from https://codeload.github.com/hokaccha/nodebrew/tar.gz/v1.0.1
#=#=#                                                                         
==> Caveats
You need to manually run setup_dirs to create directories required by nodebrew:
 /usr/local/opt/nodebrew/bin/nodebrew setup_dirs

Add path:
 export PATH=$HOME/.nodebrew/current/bin:$PATH

To use Homebrew's directories rather than ~/.nodebrew add to your profile:
 export NODEBREW_ROOT=/usr/local/var/nodebrew

Bash completion has been installed to:
 /usr/local/etc/bash_completion.d

zsh completions have been installed to:
 /usr/local/share/zsh/site-functions
==> Summary
🍺  /usr/local/Cellar/nodebrew/1.0.1: 8 files, 38.6KB, built in 2 seconds

Nodebrewがインストールされたっぽいですね
下記コマンドを実行して、問題なくインストールされているか確認してみましょう

$ nodebrew -v

バージョン番号をはじめ、コマンド一覧などが出てきたら、Nodebrewがインストールされています🎉
また、brewコマンドでも確認できます

$ brew list
nodebrew        <- Nodebrew入ってる!

次に環境変数へNodebrew関連のものを追加します

$ vi ~/.bash_profile

viコマンドで直接下記のパスを追加します

export PATH=$HOME/.nodebrew/current/bin:$PATH

追加したらsourceコマンドで更新して、この設定を反映させます

source ~/.bash_profile

最後にset upしてNodebrewのインストール〜設定完了です!

$ nodebrew setup
Fetching nodebrew...
Installed nodebrew in $HOME/.nodebrew

========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

さっき更新した設定がset upされていることが確認できます

Nodebrewでnode.jsを再インストールする

Nodebrewもインストールできたので、これでやっとpkg版node.jsに代わる、 Nodebrew版node.jsインストールの準備が整いました

まずはインストールするnode.jsの格納先を作りましょう

$ mkdir -p ~/.nodebrew/src

Nodebrewでインストールできるnode.jsのバージョンを確認してみます

$ nodebrew ls-remote
v0.0.1   
 v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6    

v0.1.0    v0.1.1    v0.1.2    v0.1.3    v0.1.4    v0.1.5    v0.1.6    v0.1.7
v0.1.8    v0.1.9    v0.1.10   v0.1.11   v0.1.12   v0.1.13   v0.1.14   v0.1.15
v0.1.16   v0.1.17   v0.1.18   v0.1.19   v0.1.20   v0.1.21   v0.1.22   v0.1.23
v0.1.24   v0.1.25   v0.1.26   v0.1.27   v0.1.28   v0.1.29   v0.1.30   v0.1.31
v0.1.32   v0.1.33   v0.1.90   v0.1.91   v0.1.92   v0.1.93   v0.1.94   v0.1.95
v0.1.96   v0.1.97   v0.1.98   v0.1.99   v0.1.100  v0.1.101  v0.1.102  v0.1.103
v0.1.104  

v0.2.0    v0.2.1    v0.2.2    v0.2.3    v0.2.4    v0.2.5    v0.2.6    

v0.3.0    v0.3.1    v0.3.2    v0.3.3    v0.3.4    v0.3.5    v0.3.6    v0.3.7
v0.3.8    

v0.4.0    v0.4.1    v0.4.2    v0.4.3    v0.4.4    v0.4.5    v0.4.6    v0.4.7
v0.4.8    v0.4.9    v0.4.10   v0.4.11   v0.4.12

​~~~中略~~~
​

上記のように、ずらずら〜っとバージョン番号が並びます
その中から使いたいバージョン番号を選んで、

$ nodebrew install <バージョン番号>

でインストールします
ちなみに、安定版のインストールは、

$ nodebrew install-binary stable

最新版のインストールは、

$ nodebrew install-binary latest
Installed successfully

「successfully」とあるので、インストール成功したっぽいですね
問題なく指定したバージョン番号のnode.jsがインストールできているか確認してみましょう

$ nodebrew ls

上記コマンドを実行して、先ほどインストールしたバージョン番号が表示されれば、問題なくインストールできています🎉
インストールしたnode.jsのバージョンをcurrentに設定します

$ nodebrew use <使いたいバージョン番号>

で設定し、

$ nodebrew ls

で出てくる「current」に使いたいバージョン番号が表示されていれば、晴れて入れ替え完了です!🎉
currentに表示されているnode.jsのバージョンが使われます

$ node -v
$ npm -v

で使用されるバージョンが確認できます

node.jsのバージョンを変えてみる

せっかくNodebrewでnode.jsのバージョン管理が楽になったので、お試しでバージョンを変えてみましょう

$ nodebrew ls-remote

でインストールできるバージョンを確認して…

$ nodebrew install v13.13.0

試しで入れてみる「v13.13.0」を指定してインストールして…

$ nodebrew ls
v13.13.0
v14.5.0    <- 先に入れていたバージョン

current: v14.5.0    <- 先に設定していたバージョン

問題なく追加で入れたバージョンが表示されていることを確認して…

$ nodebrew use v13.13.0

追加で入れたバージョンを設定して…

$ nodebrew ls
v13.13.0    <- 追加で入れたバージョン
v14.5.0

current: v13.13.0    <- 追加で入れたバージョン

できました!🎉
複数のバージョンが保持できて、しかも切り替えも簡単ですよね!

一応それぞれのコマンドでもバージョン確認

$ node -v
v13.13.0
$ npm -v
6.14.4

最新版だとうまく動かない…や、
様々なバージョンで動作確認したい!などに当てはまる方には、入れ替え超おすすめします!


コロナで危険度の高い人を見つけたらすぐ治療しようとするアプリ作ってみた

$
0
0

はじめに

この記事は chillSAP 夏の自由研究2020の記事として執筆しています。

猛暑が続いておりますが、皆さまいかがお過ごしでしょうか。
今年はコロナの影響で、日々言い知れぬ不安に苛まれることも多いのではないでしょうか。
この状況下で発熱してしまった方のご心境は察するに余りあります。
このような方の症状を一刻でも早く緩和させるため、今回は特に治療の緊急度の高いご高齢&高熱の方を判定してイ〇ジンをかけて治療を行うアプリを開発しました。
(注:本記事はコロナウイルスに対する治療法を提案するものでもなければ、イ〇ジンの効果を証明するものでも無いことをご承知おきください。また、ご気分を害された方がいらっしゃったら誠に申し訳ございません。)

概要

コロナアーキ.png

Raspberry Piに接続されたカメラモジュール及びサーマルカメラモジュールにより、測定者の年齢を及び体温を推測します(①、②)。
推定年齢及び体温が基準値を超えた場合は危険性ありと判断し、サーボモータを介して水鉄砲からイ〇ジンを発射します(③)。
またイ〇ジンが足りなくなると困るので、同時にSAP Cloud Platform上のNode.jsアプリケーションを経由して(④)、SAP S/4HANA上の購買依頼伝票作成ODataサービスから購買依頼伝票を作成して購入を進めます(⑤)。

年齢推定や体温測定の結果はRaspberry Pi上でこんな感じで表示されます。
テスト_顔ペイント前.gif

必要なもの

製品補足
Raspberry Pi 4 model:B
サーマルカメラモジュール (MLX90640)
Raspberry Pi Camera V2
サーボモータ(MG996R)それなりの力が出ないとと水鉄砲のトリガーを引けません
水鉄砲
イ〇ジン(ポピドンヨード入り) キッコーマン醤油
SAP S/4 HANA 1709   オンプレミスver

実装手順

1. Raspberry Piへの各種モジュールの接続

今回はカメラモジュール、サーマルカメラモジュール、サーボモータをRaspberry Piに接続します。
またイ〇ジン発射用の水鉄砲を適当な台に取り付け、サーボモータと接続します。
全体.jpg

Raspberry Piへの各種モジュールの接続は以下の端子及びGPIOピンを使用しています。
カメラモジュール:ラズパイのカメラ用端子
サーマルカメラモジュール:SDAをGPIO 2ピン、SCLをGPIO 3ピン
サーボモータ:GPIO 18ピン

サーボモータと水鉄砲との接続は強めの糸で行い、サーボモータが半回転したときにトリガーが引かれるようにしておきます。
水鉄砲のトリガーは基本的に結構固く、生半可な接続だと糸がずれてしまったりトリガーでは無く水鉄砲やサーボモータ自体が動いてしまうので、しっかり固定するようにしましょう。
サーボ+水鉄砲

また、水鉄砲の中には今回の主役であるキッコーマン醤油イ〇ジン(ポピドンヨード入り)を充填しておきましょう。
イ〇ジン

2. Pythonアプリの実装

Raspberry Piに接続した各種モジュールはPythonで記述したスクリプトにより制御します。
Pythonアプリの構成は以下の通りです。

.
├── face
│    └── face.py
├── httpclient
│    └── purchase_create.py
├── templates
│    └── index.html
├── thermal_camera
│    └── thermal_camera.py
├── watergun
│    └── watergun.py
└── camera_app.py

複数のカメラからの動画を別々のウィンドウに表示するのであればopencvのimshowメソッドを使用するだけでOKなのですが、今回は1画面内で(かつブラウザからサーバにアクセスして)複数の動画を表示したかったため、Python用の軽量WebアプリケーションフレームワークであるFlaskを利用しています。

まずエンドポイントに対してリクエストを送った際に表示される画面をindex.html内に記載します。

index.html
<html><head><title>Video Streaming Demonstration</title></head><body><imgsrc="{{ url_for('video_feed1') }}"><imgsrc="{{ url_for('video_feed2') }}"></body></html>

index.html 内のimgタグで動画(厳密にはカメラモジュールで撮影した動画の1フレーム)を表示するようになっています。
ソースとなる動画は、url_forで指定したルート名と同一のルートをcamera_app.pyから探して取得しています。

camera_app.py
#!/usr/bin/env python
importosfromflaskimportFlask,render_template,Responseimportadafruit_mlx90640importtimefromthermal_camera.thermal_cameraimportThermalCamerafromface.faceimportFacefromhttpclient.purchase_createimportHttpClientfromwatergun.watergunimportWatergunapp=Flask(__name__)age=0temperature=0last_posted_time=time.time()@app.route('/')defindex():"""Video streaming home page."""returnrender_template('index.html')defgenerate(camera,value):"""Video streaming generator function."""whileTrue:#画像のバイナリデータを取得
frame=camera.get_frame()#推定年齢と体温を取得する
globalageglobaltemperaturegloballast_posted_timeifvalue=='face':age=frame[1]elifvalue=='temperature':temperature=frame[1]#推定年齢が40歳以上、体温が36℃であり、前回連携時間から5秒以上が経過している場合
ifage>=40andtemperature>=36and(time.time()-last_posted_time)>=5:last_posted_time=time.time()#水鉄砲を発射
Watergun.shoot()#購買依頼伝票を作成
HttpClient.createPurchaseReq()yield(b'--frame\r\n'b'Content-Type: image/jpeg\r\n\r\n'+frame[0]+b'\r\n')@app.route('/video_feed1')defvideo_feed1():"""Video streaming route. Put this in the src attribute of an img tag."""returnResponse(generate(Face(),'face'),mimetype='multipart/x-mixed-replace; boundary=frame')@app.route('/video_feed2')defvideo_feed2():"""Video streaming route. Put this in the src attribute of an img tag."""returnResponse(generate(ThermalCamera(),'temperature'),mimetype='multipart/x-mixed-replace; boundary=frame')if__name__=='__main__':app.run(host='0.0.0.0',threaded=True,port=5000)

camera_app.pyでは、①Flaskアプリを起動、②顔認識プログラムからの画像及び推定年齢取得、③体温測定プログラムからの画像及び体温取得、④水鉄砲からのイ〇ジン噴射、⑤購買依頼伝票作成用APIへのリクエスト送信を行っています。
②③についてですが、バイト型の動画フレームをyieldで逐次出力しており、その際mimetypeをmultipart/x-mixed-replaceと指定することでサーバが紙芝居的にレンダリングするようにしています。

thermal_camera.pyではサーマルカメラモジュールからバイナリイメージを取得し、温度の情報と共に呼び出し元に返す実装がなされています。

thermal_camera.py
#!/usr/bin/env python3
importcv2importtimefromPILimportImage,ImageDraw,ImageFontimportosimportboardimportbusioimportadafruit_mlx90640importmathimportnumpyclassThermalCamera:# some utility functions
defconstrain(self,val,min_val,max_val):returnmin(max_val,max(min_val,val))defmap_value(self,x,in_min,in_max,out_min,out_max):return(x-in_min)*(out_max-out_min)/(in_max-in_min)+out_mindefgaussian(self,x,a,b,c,d=0):returna*math.exp(-((x-b)**2)/(2*c**2))+ddefgradient(self,x,width,cmap,spread=1):width=float(width)r=sum([self.gaussian(x,p[1][0],p[0]*width,width/(spread*len(cmap)))forpincmap])g=sum([self.gaussian(x,p[1][1],p[0]*width,width/(spread*len(cmap)))forpincmap])b=sum([self.gaussian(x,p[1][2],p[0]*width,width/(spread*len(cmap)))forpincmap])r=int(self.constrain(r*255,0,255))g=int(self.constrain(g*255,0,255))b=int(self.constrain(b*255,0,255))returnr,g,bdefget_frame(self):whileTrue:INTERPOLATE=10# MUST set I2C freq to 1MHz in /boot/config.txt
i2c=busio.I2C(board.SCL,board.SDA)# low range of the sensor (this will be black on the screen)
MINTEMP=20.0# high range of the sensor (this will be white on the screen)
MAXTEMP=45.0# the list of colors we can choose from
heatmap=((0.0,(0,0,0)),(0.20,(0,0,0.5)),(0.40,(0,0.5,0)),(0.60,(0.5,0,0)),(0.80,(0.75,0.75,0)),(0.90,(1.0,0.75,0)),(1.00,(1.0,1.0,1.0)),)# how many color values we can have
COLORDEPTH=1000colormap=[0]*COLORDEPTHforiinrange(COLORDEPTH):colormap[i]=self.gradient(i,COLORDEPTH,heatmap)# initialize the sensor
mlx=adafruit_mlx90640.MLX90640(i2c)mlx.refresh_rate=adafruit_mlx90640.RefreshRate.REFRESH_2_HZframe=[0]*768try:mlx.getFrame(frame)exceptValueError:continue# these happen, no biggie - retry
pixels=[0]*768fori,pixelinenumerate(frame):coloridx=self.map_value(pixel,MINTEMP,MAXTEMP,0,COLORDEPTH-1)coloridx=int(self.constrain(coloridx,0,COLORDEPTH-1))pixels[i]=colormap[coloridx]forhinrange(24):forwinrange(32):pixel=pixels[h*32+w]image=Image.new("RGB",(32,24))image.putdata(pixels)#numpy配列(RGBA) <- PILイメージ
img_numpy=numpy.asarray(image)#numpy配列(BGR) <- numpy配列(RGBA)
img_numpy_bgr=cv2.cvtColor(img_numpy,cv2.COLOR_RGBA2BGR)img=cv2.resize(img_numpy_bgr,(320,240))#画像の中から温度の最大値を取得
max_temperature=numpy.max(frame)#温度をテキストから画像に変換            
canvasSize=(320,60)backgroundRGB=(255,255,255)textRGB=(0,0,0)ifmax_temperature>=36.0:textRGB=(225,0,0)backgroundRGB=(255,255,0)else:textRGB=(0,0,0)backgroundRGB=(255,255,255)text="体温は"+str(max_temperature)[:4]+"℃です。"image_temperature=Image.new('RGB',canvasSize,backgroundRGB)draw=ImageDraw.Draw(image_temperature)font=ImageFont.load_default()font=ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf",30)draw.text((1,1),text,fill=textRGB,font=font)#numpy配列(RGBA) <- PILイメージ
img_numpy_temperature=numpy.asarray(image_temperature)#numpy配列(BGR) <- numpy配列(RGBA)
img_temperature=cv2.cvtColor(img_numpy_temperature,cv2.COLOR_RGBA2BGR)#サーマル画像と温度テキスト画像を結合する
img_concatenated=cv2.vconcat([img,img_temperature])# encode as a jpeg image and return it
returncv2.imencode('.jpg',img_concatenated)[1].tobytes(),max_temperature

face.pyについては諸事情によりソースコードを非公開とさせていただきます。中身としてはカメラモジュールから取得した画像から顔認識を行い、年齢を推定した上で顔パーツをプロットしたバイナリイメージ、及び推定年齢を返す実装がなされています。

watergun.pyではサーボモータを適切な角度に動かす実装がなされており、これにより接続された水鉄砲のトリガーが引かれます。
ご使用の水鉄砲によって角度等の設定が変わってきます。

watergun.py
importpigpioimporttimeclassWatergun:defshoot():gpio_pin0=18pi=pigpio.pi()pi.set_mode(gpio_pin0,pigpio.OUTPUT)# GPIO18: 50Hz、duty比2.5%
pi.hardware_PWM(gpio_pin0,50,25000)time.sleep(2)# GPIO18: 50Hz、duty比7.25%
pi.hardware_PWM(gpio_pin0,50,72500)pi.set_mode(gpio_pin0,pigpio.INPUT)pi.stop()

purchase_create.pyでSAP Cloud Platform上のNode.js APIを叩き、S/4HANA上で購買依頼伝票を作成します。
HttpClientでアクセスしているAPIのエンドポイントは「3. SAP Cloud SDK for JavaScriptを用いたAPI実装」でAPIをSAP Cloud Foundry上にデプロイした後にセットしてください。

purchase_create.py
importurllib.requestimporturllib.errorimportlxmlimportbase64importjsonimportreimportsslfrombs4importBeautifulSoupfromcollectionsimportdefaultdictclassHttpClient:#POSTメソッドによる購買依頼伝票の作成
defcreatePurchaseReq():url=<<APIのエンドポイント>>#ヘッダ情報を設定
headers={"Content-Type":"application/json"}#ボディ情報を設定
json_obj={"matnr":"300002","menge":1,"meins":"PC"}# POSTリクエスト送信        
json_body=json.dumps(json_obj).encode("utf-8")req=urllib.request.Request(url,data=json_body,headers=headers,method='POST')try:withurllib.request.urlopen(req)asresponse:#レスポンスを取得する        
response_body=response.read().decode("utf-8")print()# JSONとして読み込む
json_obj=json.loads(response_body)# 値の取り出し
pr_number=json_obj['matnr']print('購買依頼伝票:'+pr_number+'を作成しました。')#レスポンスから購買依頼伝票番号を取得する
#例外処理        
excepturllib.error.HTTPErroraserr:soup=BeautifulSoup(err,"lxml")print(soup)

3. SAP Cloud SDK for JavaScriptを用いたAPI実装

次にS/4 HANA上の購買依頼伝票作成用ODataサービスを呼び出すためのAPIを実装していきます。
普段はSAP Cloud SDKの中でもJavaの方を用いてS/4 HANA上にアクセスしていますが、最近NodeJsを触る機会があったので今回はSAP Cloud SDK for JavaScriptを用いてAPIを作成していきたいと思います。
(ODataサービスは相も変わらず「だっふんだ 」と言うとSAP S/4HANAで購買依頼伝票を打てるiOSアプリを実装する(2/2)で紹介されているものを使用しています。)

まず最初にコマンドライン上で以下を実行してNodeプロジェクトを立ち上げます。

$ npm init

次に必要なモジュールを順にインストールしていきます。
まずはS/4接続に必須のcloud-sdk-generatorをインストールします。

$ npm install @sap/cloud-sdk-generator

このライブラリを使用することでODataサービスのメタデータからVDMを作成したりS/4 HANAに簡単にアクセスすることが出来ます。
試しにルートにedmxフォルダを作成し、その中に拡張子がedmxのファイル(中身はODataサービスのメタデータをコピペ)を置いて以下のコマンドを実行しましょう。

$ npx generate-odata-client --inputDir edmx --outputDir vdm

するとルートにvdmというフォルダが作成され、edmxフォルダ下のメタデータに対応するvdmが作成されていることが分かります。
edmx_vdm.png

次にTypeScriptのコンパイルや実行に必要なモジュールをインストールしましょう。

$ npm install --save-dev typescript ts-node

その後tscを用いてコンパイルに関する設定ファイルであるtsconfig.jsonファイルを作成します。

$ npx tsc --init

tsconfig.jsonを開き、"experimentalDecorators"と"emitDecoratorMetadata"を有効にしておきましょう。

tsconfig.json
.../*ExperimentalOptions*/"experimentalDecorators":true,/*EnablesexperimentalsupportforES7decorators.*/"emitDecoratorMetadata":true,/*Enablesexperimentalsupportforemittingtypemetadatafordecorators....

次に、ビルド時に最初にcleanを行うためにrimrafをインストールします。

$ npm install rimraf

また今のうちにpackage.jsonの"scripts"を修正してコマンドライン上での実行が楽になるようにしておきます。

package.json
..."scripts":{"vdm":"npx generate-odata-client --inputDir edmx --outputDir vdm --forceOverwrite","clean":"rimraf lib reports","build":"npm run clean && npx tsc","start":"node ./src/index.js","test":"echo \"Error: no test specified\"&& exit 1"},...

最後に、APIの実装に必要なモジュールをインストールしておきます。

$ npm install routing-controllers reflect-metadata typeorm

Node.jsアプリケーション実行時に起動されるロジックをindex.tsに記載します。

index.ts
import"reflect-metadata"import{createExpressServer}from"routing-controllers";import{PurchaseRequestController}from'./PurchaseRequestController';constapp=createExpressServer({controllers:[PurchaseRequestController]});app.listen(8080);

Entity、Controller、Serviceを必要に応じて実装します。

PurchaseRequestEntity.ts
import{Entity}from'typeorm';@Entity()exportclassPurchaseRequestEntity{matnr:string;menge:number;meins:string;constructor(matnr:string,menge:number,meins:string){this.matnr=matnr;this.menge=menge;this.meins=meins;}}
PurchaseRequestController.ts
import{JsonController,Get,Post,Body}from'routing-controllers';import{PurchaseRequestEntity}from'./PurchaseRequestEntity';import{PurchaseRequestService}from'./PurchaseRequestService';@JsonController("/tr")exportclassPurchaseRequestController{@Get("/all")getAllReq(){returnnewPurchaseRequestService().getAllPurchaseRequest();}@Post("/single")createReq(@Body()pr:PurchaseRequestEntity){returnnewPurchaseRequestService().createPurchaseRequest(pr);}}
PurchaseRequestService.ts
import{PurchaseReqSet}from'../vdm/ypurchase-request-create-service/PurchaseReqSet';import*asPRSrvfrom'../vdm/ypurchase-request-create-service';import{BigNumber}from'bignumber.js';import{PurchaseRequestEntity}from'./PurchaseRequestEntity';exportclassPurchaseRequestService{getAllPurchaseRequest(){returnPRSrv.PurchaseReqSet.requestBuilder().getAll()//照会処理用のメソッド.top(10).select(PRSrv.PurchaseReqSet.ALL_FIELDS).execute({destinationName:'<<Destination名>>'});}createPurchaseRequest(pr:PurchaseRequestEntity){constpurchaseReqSet=PRSrv.PurchaseReqSet.builder().matnr(pr.matnr).menge(newBigNumber(pr.menge)).meins(pr.meins).build();returnPurchaseReqSet.requestBuilder().create(purchaseReqSet).execute({destinationName:'<<Destination名>>'});}}

さて、CF環境へのデプロイに必要不可欠なmanifest.ymlをルートディレクトリに追加しましょう。
ここにConnectivityやDestinationをバインドするよう記述しても良いですが、今回は面倒なのでデプロイ後にマニュアルでSAP Cloud Platform上からバインドしています。

manifest.yml
---applications:-name:ynodejs_purchase_requestmemory:128Mbuildpacks:-https://github.com/cloudfoundry/nodejs-buildpack

最終的なディレクトリ構成は以下の通りです。

.
├── edmx
│    └── YPURCHASE_REQUEST_CREATE_SRV.edmx
├── node_modules
├── src
│    ├── index.ts
│    ├── PurchaseRequestController.ts
│    ├── PurchaseRequestEntity.ts
│    └── PurchaseRequestService.ts
├── vdm
├── manifest.yml
├── package.json
├── package-lock.json
└── tsconfig.json

ビルドまで完了したらSCP Cloud Foundryにデプロイします。ルート階層でcf pushを行いましょう。manifest.yml内に記載したアプリ名を指定すればOKです。

$ cf push ynodejs_purchase_request

これでS/4HANA上で購買依頼伝票を作成するためのAPIが完成しました。
※「2. Pythonアプリの実装」のpurchase_create.py内でStubで置いていたAPIのエンドポイントを書き換えてください。

完成

同期の間で老け顔キャラだったのですが、機械にまで実年齢+7歳くらいで評価されたのは地味にショックでした。


Node.jsでOSの環境変数を読み込む

$
0
0
yuta:~ $ node
> process.env
{ SHELL: '/bin/bash',
  SESSION_MANAGER:
   'local/CARMILLA:@/tmp/.ICE-unix/1792,unix/CARMILLA:/tmp/.ICE-unix/1792',
  QT_ACCESSIBILITY: '1',

実行

app.js
console.log(process.env.USER)
yuta:~ $ node app.js 
yuta

[JavaScript]ESLintの設定をグローバルルールで保存する

$
0
0

初めに

学習用でESLintが使いたいけど、プロジェクトごとに設定ファイル(.eslintrc.*)を作成するのは面倒。
そこで、設定ファイルをグローバルルールとして保存してプロジェクトごとに作成しなくていいようにしたので、その時にやった方法を備忘録としてまとめてみました。

方法

1. ESLintのインストール

まずはESLintをグローバルインストール。

>npm -g i eslint

ここで、-gは--global、iはinstallを省略したもの。

2. ESLintの設定ファイルをグローバルとして保存

.eslintrc.jsonを作成し、ユーザーディレクトリ直下に保存する。

  • .eslintrc.jsonを保存する場所
MacOS:/ Users / {USERNAME} 
Windows:C:\ Users \ {USERNAME}
  • .eslintrc.jsonの中身
.eslint.json
{"extends":"eslint:recommended","env":{"node":true,"es6":true},"rules":{"semi":["error","never"]}}

各設定の簡単な説明

  • extendsプロパティ
    共有設定を指定する。
    eslint:recommendedはESLintの推奨ルールがセットになったもの。

  • envプロパティ
    環境を指定する。

    • "node": true
      Node.js 固有の変数や構文 (requireや特殊なトップレベル スコープ等) が定義され、利用できるようになる。
    • "es6": true
      ECMAScript 2015(ES6)で追加された構文や組込みオブジェクトが利用できるようになる。
  • rulesプロパティ
    ルールを設定する。
    大体のルールはextendsプロパティで設定されるため、extendsで設定されていないルールを追加したいときや設定しているルールを省きたいときに利用する。
    また、extendsで共有設定を指定しない場合はここで一からルールを設定する。

    • semiプロパティ
      セミコロンを付けるか付けないかの設定をする。
      "error"でルールが守れてないときはエラー表示。
      "never"で文末のセミコロン;を禁止する。

3. ESLintの実行

JavaScriptのプログラムを作成してグローバルルールが通っているか確認する。
sample.jsを作成し保存。

sample.js
console.log("hello world");

sample.jsが保存されているディレクトリに移動して以下のコマンドを実行。

>eslint sample.js

グローバルルールが通っていれば以下のようなエラーが表示されるはず。

  1:27  error  Extra semicolon  semi

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

ejsでインクルードをするときに、パスをルート相対URLで指定したい

$
0
0

process.cwd()

Node.jsのprocess.cwd()は、コマンドが実行されたディレクトリを返します。

たとえば、次のようなファイル構成だったとしましょう。

./project
└ src/
 └ *.ejs
└ package.json
└ gulpfile.js

この場合、projectディレクトリでgulpコマンドをたたくことになります。その時にprocess.cwd()が返す値はpath/to/projectになるので、次のように1つ変数化しておけばルート相対URLのような感覚でパス名を記述することができます。

<%constROOT=`${process.cwd()}/src`;%><%-include(`${ROOT}/path/to/_include.ejs`)-%><%-include(`${ROOT}/path/to/_include.ejs`)-%><%-include(`${ROOT}/path/to/_include.ejs`)-%><%-include(`${ROOT}/path/to/_include.ejs`)-%>

JSON.stringifyで`TypeError: Converting circular structure to JSON`というエラーが出た時の対処法

$
0
0

オブジェクトをJSON.stringifyしたときに、循環参照が含まれていると以下のようなエラーが出ます。

コード
consta={a1:"test"};a.aa=a;JSON.stringify(a);
実行結果
JSON.stringify(a);
     ^

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'--- property 'aa' closes the circle
    at JSON.stringify (<anonymous>)

対処法

json-cyclicを使えば解決します。

json-cyclic - npm
https://www.npmjs.com/package/json-cyclic

npm install json-cyclic
コード
const{decycle,encycle}=require('json-cyclic');consta={a1:"test"};a.aa=a;console.log(JSON.stringify(decycle(a)));
実行結果
{"a1":"test","aa":{"$ref":"$"}}

json-cyclicは、内部で循環参照を検出したとき、復元ができるようにタグ付けを行い、参照を削除する仕組みです。

オブジェクトを復元したい場合は以下のようにencycleを使います。

コード
const{decycle,encycle}=require('json-cyclic');consta={a1:"test"};a.aa=a;constjson=JSON.stringify(decycle(a));constobj=JSON.parse(json);constb=encycle(obj);console.log(b);console.log(b.aa.aa.aa.a1);
実行結果
{ a1: 'test', aa: [Circular] }test

ちゃんと復元できることが確認できました。

npm install --save firebase で発生したエラーの回避法

$
0
0

記事執筆時点で安定版のfirebase 6.2.4をインストールを試みたところ以下のエラーが出ました。

$ npm install --save firebase@6.2.6
:
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! grpc@1.20.3 install: `node-pre-gyp install --fallback-to-build --library=static_library`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the grpc@1.20.3 install script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/yokoyamaryouta/.npm/_logs/2020-05-10T05_49_16_325Z-debug.log

結論

安定版で実行したらインストールできました。以下から確認&試してみて下さい。

$ node -v
v14.8.0 //筆者の場合

こちらから確認&インストールできます。
安定版インストール後に再度実行したらいけました。

$ node -v
v12.18.3 //執筆時
$ npm install --save firebase@6.2.6
Viewing all 8835 articles
Browse latest View live