- 投稿日:2020-05-17T23:32:48+09:00
Arduinoの測定値をNode.jsで受けてSocket.ioとchart.jsでリアルタイムにグラフ表示
概要
Arduinoの測定値をリアルタイムにグラフ表示してみたかったので、Node.js諸々を用いてブラウザ上にグラフ表示してみました。
今回はひとまず光センサーを測定対象にしました。光センサーの抵抗値の変化を電圧としてArduinoのアナログ入力で測定しています。普段コーディングしない人間のコードなので変な箇所が多々あるかもしれません。その辺はご了承ください。
構成
ハードウェアとソフトウェアの構成を示します。Arduino周りの回路はDEVICE PLUSの記事を参考にしてください。
ハードウェア
- PC (Mac)
- Arduino UNO (PCとUSB接続)
- 光センサー回路
- ブレッドボード
- 光センサー
- 抵抗(1kΩくらい)
- ジャンパー線
ソフトウェア
- Node.jsのフレームワークであるExpress
- Arduinoとシリアル通信するためのserialportライブラリ
- リアルタイム通信するためのSocket.ioライブラリ
- グラフ表示するためのchart.jsライブラリ
ソースコード
ソースコードは以下の4つです。
- Arduino
- serialCom.ino
- サーバ側
- app.js
- クライアント(ブラウザ)側
- index.html
- index.js
ディレクトリ構造. ├── app.js ├── index.html ├── node_modules │ ├── @serialport │ : │ └── yeast ├── package-lock.json ├── package.json └── public └── index.jsArduino
serialCom.inoint analogPin=A3; double aval=0; double val=0; void setup() { Serial.begin(9600); } void loop() { aval = analogRead(analogPin); val = 5 * aval / 1024; Serial.println(val); delay(100); }アナログ入力のA3ピンと5V出力を使用しています。(使用する端子は回路によって変わります。)
analogReadで得られる数値は10bitのA/Dコンバータの出力コードなので、電圧に変換しています。測定間隔は100msにしました。サーバ側
app.jsvar express = require('express'); var app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); const SerialPort = require('serialport'); const Readline = require('@serialport/parser-readline'); const port = new SerialPort('/dev/cu.usbmodem141101', { baudRate: 9600 }); const parser = new Readline(); port.pipe(parser); app.use(express.static('public')); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); }); // ブラウザ側とのコネクション確立 io.on('connection', (socket) => { console.log('a user connected'); socket.on('disconnect', () => { console.log('user disconnected'); }); }); //サーバ起動 http.listen(3000, function(){ console.log('listening on *:3000'); }); //Arduinoからデータを受信したらクライアントへ送信 parser.on('data', (data) => { io.emit('graph update', (data)); });必要なモジュールをインポートしてそれぞれ設定します。
Arduinoからのデータをシリアルポートで待ち受けます。(ポート名は環境によって変わります。)
httpサーバのポート3000で待ち受けます。
socket.ioでクライアントのブラウザと接続します。
Ardionoからシリアルポート経由でデータを受信したらブラウザへデータを送信します。クライアント側
index.html<!doctype html> <html> <head> <title>グラフテスト</title> <script src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.0/Chart.min.js'></script> <script src='//code.jquery.com/jquery-3.2.1.min.js'></script> <script src='http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.2/moment.min.js'></script> <script src='//cdn.socket.io/socket.io-1.4.5.js'></script> </head> <body> <canvas id="canvas" width="500" height="500"></canvas> <script src='index.js'></script> </body> </html>jquery, chart.js, moment.js, socket.ioを読み込んでいます。
(moment.jsをバンドルしたchart.jsもあるみたいですが、今回はそのまま。)
canvasタグ内にchart.jsでグラフが描画されます。
script部分は別ファイルにしています。index.jsconst socket = io.connect(); var ctx = document.getElementById('canvas').getContext('2d'); // グラフの作成 var myChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'data-label1', data: [], backgroundColor: 'rgba(0,0,225,1)', borderColor: 'rgba(0,0,225,1)', borderWidth: 1, lineTension: 0, fill: false }] }, options: { title: { display: true, text: 'CHART TITLE' }, scales: { xAxes: [{ ticks: { //autoSkip: true, maxTicksLimit: 10 } }], yAxes: [{ ticks: { // beginAtZero:true, // autoSkip: true, // maxTicksLimit: 10, min:0, max:5, stepSize:1 } }] }, // グラフサイズ固定 responsive: false, //maintainAspectRatio: false } }); $(() => { // サーバから値を受け取った時の処理 socket.on('graph update', (recievedData) => { // 現在時刻の取得 const time = moment(); const outputTime = time.format('HH:mm:ss'); // 追加するデータのラベルに時間を追加 myChart.data.labels.push(outputTime); // グラフにデータを追加 myChart.data.datasets[0].data.push(recievedData); // データ数が100以上なら一番古い要素を削除 if (myChart.data.datasets[0].data.length > 100) { myChart.data.labels.shift(); myChart.data.datasets[0].data.shift(); }; // グラフの表示を更新 myChart.update(); }) });myChartがグラフの設定です。
その後に続くのがサーバからデータを受け取った時の処理です。
moment.jsでラベルに現在時刻を追加しています。
無限にデータが増え続けるので、データ数100を上限にしています。結果
ArduinoをUSBで接続してから、ターミナルで
node app.js
と叩いてサーバを起動、ブラウザで127.0.0.1:3000
へアクセスするとこんな感じのグラフが表示されます。
光センサーの上で手で光を遮ったりしたグラフになります。光を遮ると光センサーの抵抗値が上がるので電圧も上がります。
横軸がなぜか均等にならないのですが、気が向いたら改善します。
- 投稿日:2020-05-17T22:39:14+09:00
【初心者向け】記念日を通知するLINE botをheroku + Node.jsで作る
概要
この記事では、HerokuとNode.jsを活用して特定のユーザ(少人数を想定)に記念日を通知するLINE Botを作るノウハウを紹介します。ここでの通知とは、時間指定でbotからユーザへの簡単なテキストメッセージを送ることを指します。
事前準備1 LINE bot のチャネルの用意
LINE Botを作成するためにはLINEのMessaging APIというサービスを使います。
まずこのサービスを利用するためのチャネルを作成します。
以下をページを参考に必要な情報を入力して進めてください。実在するアカウントと同様にアイコンも設定できます。
ここでは、某作品より以下のようなアイコンを設定しました。
Messaging APIには、botからユーザへのメッセージ(Push)とユーザがbotに送ったメッセージに対する返信(Reply)
がありますが、今回使用するのは、Pushの方です。
事前準備2 ホストするサーバー(Heroku)の用意
次に、botをホストするサーバを用意します。
今回は無料で多くのサービスが利用可能なHerokuを使います。
Herokuの基本的な使い方やデプロイするまでの流れは、以下の記事が参考になると思います。https://qiita.com/arashida/items/b2f2e01259238235e187
https://qiita.com/shti_f/items/b4b5d830672d908eff4ebotの開発
今回はbotからのメッセージを送ることを想定しているので、起点はHerokuのアドオン機能であるheroku schedulerを利用することにします。(詳細は後述)
この設定は後ほど行うとして、まずは通知するためのプログウラムを記載します。
通常使うindex.jsとは別にファイルを一つ作成します。コードを書くにあたっては、Herokuの環境変数の設定などがあり、その点は上記の記事を参考にさせていただきました。
sample.js#!/usr/bin/env node // モジュールのインポート const server = require("express")(); const line = require("@line/bot-sdk"); // Messaging APIのSDKをインポート // パラメータ設定 const line_config = { channelAccessToken: process.env.LINE_ACCESS_TOKEN, // 環境変数からアクセストークンセット channelSecret: process.env.LINE_CHANNEL_SECRET // 環境変数からChannel Secretをセット }; // APIコールのためのクライアントインスタンスを作成 const bot = new line.Client(line_config); main();//メインとなる処理を適当に //メッセージを送る処理 function sendMessage(message){ console.log("message:" + message); bot.pushMessage("XXXXXXXXXXXXXXXXXXXXXXXXXX",{ //送りたい相手のUserID type:"text", text: "今日ハ " + message + "ダゼェ!" }) } function main() { //現在日付の取得 var today = new Date(); var month = today.getMonth()+1; var date = today.getDate(); var message = ""; //デフォルトのメッセージをなにか入れたい場合はここに入れる。 //送るべきメッセージの判定 //XXの誕生日 if (month == 7 & date == 30 ) { message = "XXの誕生日" sendMessage(message); } //入籍届けを出した日 else if (month == 8 & date == 11 ) { message = "入籍届けを出した日" sendMessage(message); } //該当しない日は何もしない else { } }今回は簡易的な作りでDBのようなものを持たせていないので、日付の判定もメッセージ送信先のIDもハードコードしています。
package.jsonにJobの追加
heroku schedulerにキックしてもらうためのジョブと実際に動かすjsのファイルをpackage.jsonで紐付けます。
package.json{ "name": "line_botXXXXX", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "job-push-sample": "node sample.js" }, ...スケジューラの設定
スケジューラはHerokuのアドオン機能で日時や毎時の実行であれば、この機能で十分です。
※細かい設定はできないので、そのあたりまで実装したい方は別のサービスを使ったほうがいいでしょう
実際の設定は以下の記事を参考にさせていただきました。実際の設定画面は以下の通りです。
npm run 「package.jsonで指定したjob」という記載の仕方です。
※時間は標準時での設定しかできない点に要注意。
設定は以上です!
実際に指定したユーザのラインにメッセージが飛んでいることを確認できましたでしょうか?まとめ
ラインは生活の中に浸透していることもあって、アイディアと工夫次第で面白いことができそうですね。
これからも可能性を探っていきたいと思います。
- 投稿日:2020-05-17T21:09:57+09:00
AlexaスキルのHelloWorldテンプレートを日本語化する(Node.js編)
はじめに
Alexa Skills Kitコマンドラインインターフェース(ASK CLI)を使うとテンプレートに従って新しいスキルを作ることができますが、
そのテンプレートの中のHelloWorldスキルは現状US版になっています。
グローバル対応させるつもりがないときは日本語化しておきたいため、変更内容をメモしておきます。なお本記事では触れませんが、HelloWorld以外のテンプレートは日本語も含め多言語化されているので、
そこから日本語以外を削る、という方法でも対応できると思います。日本語化手順
前提
ask newでスキルのテンプレートが作られているところまでを前提とします。
公式ページ記事を書くにあたり選択したオプションは以下となります。
ask new Please follow the wizard to start your Alexa skill project -> ? Choose the programming language you will use to code your skill: NodeJS ? Choose a method to host your skill's backend resources: AWS with CloudFormation ? Choose a template to start with: Hello world Alexa's hello world skill to send the greetings to the world! ? Please type in your skill name: skill-sample-nodejs-hello-world ? Please type in your folder name for the skill project (alphanumeric): Helloworld Project for skill "skill-sample-nodejs-hello-world" is successfully created at C:\Users\xxx\xxx\Helloworld Project initialized with deploy delegate "@ask-cli/cfn-deployer" successfully.変更箇所一覧
変更箇所の一覧は以下です。
階層 ファイル 変更内容 . ask-resources.json ・AWSのリージョンを修正 skill-package skill.json ・ロケールを日本に修正 skill-package/assets en-US_largeIcon.png
en-US_smallIcon.png・リネーム skill-package/interactionModels/custom en-US.json ・リネーム
・発話を日本語化変更前にいきなりask deployをしてしまうとAWSの米国リージョンにデプロイされてしまい、
東京リージョンだけ見ていると見つけられない、ということが起きるので、初回のデプロイは日本語化の後に行うのがよいかと思います。変更内容詳細
それぞれの変更内容の詳細は以下です。
ask-resources.json
"awsRegion"をap-northeast-1にしておきます。これにより、東京リージョンにデプロイされるようになります。
ask-resources.json{ "askcliResourcesVersion": "2020-03-31", "profiles": { "default": { … }, "skillInfrastructure": { "userConfig": { "runtime": "nodejs10.x", "handler": "index.handler", "templatePath": ".\\infrastructure\\cfn-deployer\\skill-stack.yaml", "awsRegion": "ap-northeast-1" // ☆ <-- us-east-1から変更 }, "type": "@ask-cli/cfn-deployer" } } } }skill-package/skill.json
スキルの設定全般を管理するファイルです。localeをja-JPにします。
また、日本だけの対応とするため、isAvailableWorldwideをfalseに、distributionCountriesをJPにします。skill.json{ "manifest": { "publishingInformation": { "locales": { "ja-JP": { // ☆ <-- en-USから修正 "summary": "Sample Short Description", "examplePhrases": [ "Alexa open hello world", "hello", "help" ], "name": "skill-sample-nodejs-hello-world", "description": "Sample Full Description" } }, "isAvailableWorldwide": false, // ☆ <-- trueから修正 "testingInstructions": "Sample Testing Instructions.", "category": "KNOWLEDGE_AND_TRIVIA", "distributionCountries": ["JP"] // ☆ <-- "JP"を追加 }, "apis": { "custom": {} }, "manifestVersion": "1.0" } }skill.jsonは、スキルを公開するときにはより詳細に書き直す必要があります(説明や発話例を日本語化するなど)。
skill-package/assets/en-US_largeIcon.png、en-US_smallIcon.png
スキル用アイコンの画像ファイルです。そのままでも支障ないですが一応リネームしておきます。
en-US_largeIcon.png、en-US_smallIcon.png
↓
ja-JP_largeIcon.png、ja-JP_smallIcon.pngskill-package/interactionModels/custom/en-US.json
en-US.jsonからja-JP.jsonにリネームした上で内容を書き換えます。
スキルの呼び出し名などを日本語にしています(英語のままでも動きますが一応)。ja-JP.json{ "interactionModel": { "languageModel": { "invocationName": "ハローワールド", // ☆ <-- hello worldから修正 "intents": [ … { "name": "HelloWorldIntent", "slots": [], "samples": [ "こんにちは" // ☆ <-- hello などから修正 ] }, … ], "types": [] } } }このとき、ファイルをUTF-8で保存しないとデプロイ時にエラーになるので注意が必要です。
デプロイ
ここまで変更できたら、ask deployでのデプロイにより、Alexa Developer ConsoleとAWSの東京リージョン上に反映され、
日本語化されたスキルを利用できるようになるはずです。あとはどんどん書き換えていけばOKです。
- 投稿日:2020-05-17T19:15:04+09:00
ローカルパッケージをシンボリックリンクでインストールしたらTypeScriptの型定義がコンフリクトして困った
npmやyarnには、依存先のパッケージをシンボリックリンクとして
node_modules
内にインストールする手法があります。npm link
が代表的な例です。しかしながら、多くの場合この状況となるのは
package.json
にpackage.json{ "dependencies": { "bando-commons": "file:path/to/directory" } }のように指定がなされている場合です1。
この時、指定されたディレクトリが
node_modules
配下にシンボリックリンクされますが、この時指定されたディレクトリ内にあるnode_moduleが紛れ込んでしまうことに注意が必要です。project ├─ package.json ├─ node_modules │ ├─ linked_project (symlink) │ │ ├─ node_modulesのような感じです。通常の方法でインストールした場合はnpm自体の依存関係解決機能により1つのパッケージのみしかインストールされないため大丈夫なのですが。
この構成がTypeScriptの型定義ファイルに悪影響を及ぼす場合があります。具体的にはインストール先とインストール元で異なるバージョンのパッケージを使っていた場合です。この時、異なる内容の
declare module
宣言が重複するため、TypeScriptはどちらを使ってよいか判断できず困るという具合です。解決方法としては簡単で、双方の依存先のパッケージのバージョンをそろえるだけです。
package-lock.json
やyarn.lock
の内容を確認しましょう。yarnでの手法についてはこちらが参考になります。なお、関連する話題のissueがありますので併せて確認すると良いと思います。
- 投稿日:2020-05-17T15:09:53+09:00
deno で遊んでみよう
deno とは
deno は Node.js の製作者 Ryan Dahl 氏により開発された TypeScript を標準でサポートするランタイムです。
Node.js のときに得た教訓をもとに設計がされていますが、Node.js と互換性のある実装というわけではありません。
JavaScript のエンジン自体は Node.js と同じく V8 を利用しています。
公式サイトはこちら2020-05-13 にめでたく v1.0 がリリースされたので今回ちょっとだけ遊んでみることにします。
https://deno.land/v1Node.js との違い
たくさんありますが大きな違いだけ。
- TypeScript が標準でサポートされる
- 簡単なものであれば tsconfig.json の用意はしなくて良い
- パッケージの取得はソースコードの import 文をもとに deno が実行時に取得する
- スクリプトはすべて ES Module 方式になっている
- npm や yarn のようなパッケージマネージャは今の所不要
- バージョニングは URL をベースに行う
- ファイルアクセスやネットワークなどのセキュリティフラグがいくつかある
- 実行時に指定したもの以外は基本的にブロックされる
細かなところは公式サイトをご覧ください。
セットアップ
deno はシングルバイナリで動作するランタイムなので Github の Releases から取得した zip を展開して得られる実行ファイルをパスの通ったところに配置すれば直ぐに利用ができます。
公式ではこの作業を簡略化するための deno_install というプロジェクトが立ち上がっておりワンライナーでインストールが可能です。
- Linux / WSL / MacOS
curl -fsSL https://deno.land/x/install/install.sh | sh
- MacOS
brew install deno
(確認時点ではLinuxBrewには非対応)- Cargo
cargo install deno
インストール後はパスを通したりするよう指示が出たりしますが適切に対処しましょう。
正しくインストールされていればバージョン情報を確認できるようになっています。
TypeScript のバージョンまで出てるのが斬新ですね。version❯ deno --version deno 1.0.0 v8 8.4.300 typescript 3.9.2
初めての deno
公式にリモートからコードを取得して直接実行するサンプルが用意されていますので試しに実行してみましょう。
deno run https://deno.land/std/examples/welcome.ts実行すると下記のような出力を得ます。見たらわかりますが、リモートのURLを直接実行することができ、スクリプトがコンパイルされ実行されていますね。
❯ deno run https://deno.land/std/examples/welcome.ts Download https://deno.land/std/examples/welcome.ts Warning Implicitly using master branch https://deno.land/std/examples/welcome.ts Compile https://deno.land/std/examples/welcome.ts Welcome to Deno ?今回実行したスクリプトは https://deno.land/std/examples/welcome.ts ですが直接ブラウザで開くとこのように中身を確認できます。
あっけなく終わってしまいましたが deno のスクリプトを実行する事ができました。
ここまででは Node.js をインストールして サンプルスクリプトを実行したのとあまり大差ないのでもう少し遊んでみましょう。deno をより体感する
続いても公式サイトにあるサンプルスクリプトを利用します。(自分好みにスタイルを変更しています)
下記のファイルをダウンロードするか写経して server.ts として保存しましょう。serverimport { serve } from 'https://deno.land/std@0.50.0/http/server.ts' const s = serve({ port: 8000 }) console.log('http://localhost:8000/') for await (const req of s) { req.respond({ body: 'Hello World ?\n' }) }早速実行してみます。
server.tsの実行結果❯ deno run ./server.ts Compile file:///path/to/workspace/server.ts Download https://deno.land/std@0.50.0/http/server.ts ... Download https://deno.land/std@0.50.0/bytes/mod.ts error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11) at Object.sendSync ($deno$/ops/dispatch_json.ts:72:10) at Object.listen ($deno$/ops/net.ts:51:10) at listen ($deno$/net.ts:152:22) at serve (https://deno.land/std@0.50.0/http/server.ts:261:20) at file:///path/to/workspace/server.ts:2:11ぞろぞろとパッケージが取得されていき、実行されるかと思いきやエラーになりました。
これは最初に説明した deno のセキュリティ機構によるもので、deno run
したときに許可した権限以上の処理ができないようになっています。
今回の場合、サーバーアプリケーションを起動するために必要なネットワークに対する権限が実行時に付与されていないので、権限が足りないよということを指摘されています。ということで、
--allow-net
をフラグに指定して権限を追加してリトライしてみると今度はうまく起動してくれました。
試しに curl でアクセスしてみるとHello World
の出力を得ることができます。server.tsの実行結果❯ deno run --allow-net ./server.ts & http://localhost:8000/ ❯ curl localhost:8000 Hello World ?
このようにして無事Webサーバーアプリケーションを deno で作って動作させることができました。
ライブラリやエコシステムについて
deno には npm や yarn といったパッケージマネージャが現時点では存在していません。
ここまでやってきたように、deno の標準パッケージも含め、必要な外部ライブラリの類はすべて実行時にリモートから取得される仕組みになっています。加えて、deno では中央集権的なパッケージリポジトリは存在しておらず、代わりにスクリプトが HTTP でアクセス可能な場所に配置されていればそれを利用することが可能なようになっています。そのため、ライブラリの公開や利用については GitHub Pages や pika.dev or jspm.io などの CDN を利用して手軽に誰でも公開することができます。
これは Node.js において npm が実質的に中央集権的なパッケージリポジトリとなってしまったことに基づく設計だそうで、「Node.jsに関する10の反省点」でも言及されています。この方式においては npm を探して必要なパッケージを見つけるというスタイルが失われてしまいますが、それを防ぐ取り組みとして deno.land/x でパッケージを探せる仕組みが提供されています。これはパッケージそのものをホスティングしているのではなく、あくまで電話帳のような仕組みなので deno.land/x を利用してやりたいことに対応するパッケージを探したりここから利用することができるようになっています。
まとめ
この度めでたく v1.0 がリリースされた deno でちょっとだけ遊んでみました。軽く触っただけでも Node.js との違いがちょっとだけわかった気がします。今後、3rd Party ライブラリがより充実したりなど、エコシステムが拡充されてくるとより輝いてくるのではないかと期待しています。
何より標準で TypeScript が実行できる状態にあるというのが TypeScript 大好きクラブ 会員としては最高だなーという印象です。Node.js との棲み分けや移行についてが気になるところですが、少なくとも Node.js のライブラリが今すぐそのまま deno でも使えるというわけでもないので、徐々に両対応するライブラリが出てきたり、deno 専用の革新的な何かが出てきたりすることを期待しています。(deno版のElectronに相当するなにかとか)
これから普段遣いしていくぞというところも v1.0 のお墨付きがあるので、単純に書き捨てのスクリプトを TypeScript で書くという用途においてはもう deno でやったほうが手軽さがあるのではないかな思ったりしました。
- 投稿日:2020-05-17T12:22:30+09:00
M5StickCとSpeaker HatでAI Chatと会話
M5StickCにはマイクがついています。また、M5stickCの拡張端子に接続できるSpeaker Hatがあるので、それを組み合わせれば、何かできそう。
M5StickC Speaker Hat(PAM8303搭載)
https://m5stack.com/products/m5stickc-speaker-hat?_pos=7&_sid=b84fce0ec&_ss=rということで、M5StackCとSpeaker Hatを組み合わせて、Web上にいるAI Chatと会話をしてみたいと思います。
全体的な流れは以下の通りです。
音声認識には、Google Cloud Speech APIのSpeech-to-Textを利用しました。
音声合成には、AWSのAmazon Pollyを利用しました。
そして、主題のAI Chatには、ユーザローカル社の人工知能チャットボット(chatbot)を利用しました。以降は、以下の流れに沿って、補足していきます。
・M5StickCでの録音・再生
・M5StickCからWAVEデータの送信・受信
・中継サーバでのM5StickCからの受信・送信
・中継サーバでの音声認識の呼び出し
・中継サーバでのAI Chatの呼び出し
・中継サーバでの音声合成の呼び出し作っては見ましたが、マイクの音量は小さめですし、スピーカやマイクの音質はかなり悪かったです。(M5Stackでやったほうが良いかも)
ですが、かなりマイクの音質が悪いにも関わらず、GoogleのSpeech-to-Textはしっかり音声認識してくれるのは驚きました。ソースコード一式は、GitHubに上げておきました。
Swagger-nodeを使ったサーバです。Arduinoのソースコードも上げています。poruruba/m5stickc_chat
https://github.com/poruruba/m5stickc_chatM5StickCでの録音・再生
Arduinoを使います。
実装は、以下の記事を参考にさせていただき、ほぼそのまま使わせていただきました。(ありがとうございます!)M5StickCのマイクを使ってみる その3 録音再生
https://lang-ship.com/blog/work/m5stickc-mic-3/これ以上の説明はいらないぐらいシンプルにして記載いただいているので、非常に助かりました。
録音すると、8ビットのサンプリングデータが出力され、8ビットのサンプリングデータを用意すれば、再生できるところまで、関数化してくれています。
8ビットのサンプリングデータの中央値(無音)は、128です。signed charではなく、unsigned charです。あとで説明する、M5StickCからのWAVEデータの送信・受信では、WiFiを使っていまして、そうすると残メモリ容量が厳しくなってしまいました。
サンプリングレートは、8KHz、録音できる秒数を4秒にしています。ただ、スピーカの音質はかなり悪く、聞き取れないぐらいでした。(PIN設定が違うのかな???)
M5StickCからWAVEデータの送信・受信
M5StickCからの送受信の通信路には、WiFiを利用し、通信プロトコルはHTTP Postです。
また、WAVEはバイナリファイルなので、Base64にエンコードして送信し、Base64で受信するのでデコードの処理を行います。
MimeTypeは、送信が「application/json」、受信が「text/plain」です。Densaugeo/base64_arduino
https://github.com/Densaugeo/base64_arduinoライブラリマネージャから「base64」をインストールします。
HTTP Post部分のソースコードを抜粋します。
HTTPClient http; const char *host = "【中継サーバのエントリポイントのURL】"; char httpBuffer[(STORAGE_LEN + 2)/ 3 * 4]; int http_post(uint8_t *p_inout_buffer, int in_length){ strcpy(httpBuffer, "{\"message\": \""); encode_base64(p_inout_buffer, in_length, (unsigned char*)&httpBuffer[strlen(httpBuffer)]); strcat((char*)httpBuffer, "\"}"); Serial.println("HTTP Post"); http.begin(host); http.addHeader("Content-Type", "application/json"); int status_code = http.POST((uint8_t*)httpBuffer, strlen(httpBuffer)); if( status_code != HTTP_CODE_OK ){ Serial.println("Status is not 200"); http.end(); return -1; } int len = http.getSize(); WiFiClient * stream = http.getStreamPtr(); int ptr = 0; while(http.connected() && (len > 0 || len == -1)) { size_t size = stream->available(); if(size) { if(size > (sizeof(httpBuffer) - ptr) ){ Serial.println("receive overflow"); http.end(); return -1; } int c = stream->readBytes(&httpBuffer[ptr], size); ptr += c; if(len > 0) len -= c; } delay(1); } httpBuffer[ptr] = '\0'; http.end(); return decode_base64((unsigned char*)httpBuffer, p_inout_buffer); }application/jsonのBody部の生成に、ArduinoJsonを使いたかったのですが、残メモリが限界で、諦めました。受信も、application/jsonにしたかったのですが、同じ理由で、JSON解析不要のtext/plainにしています。
送信時のJSONは、
"{\"message\": \"" + base64化したWAVEデータ + "\"}"として、決め打ちのJSONにしています。
中継サーバでのM5StickCからの受信・送信
サーバには、Swagger-nodeを使っています。
Swagger定義は以下の通りです。
swagger.yaml/speech: post: x-swagger-router-controller: routing operationId: speech parameters: - in: body name: body required: true schema: type: object required: - message properties: message: type: "string" produces: - text/plain responses: 200: description: Success schema: type: stringあとは、以降で説明する、音声認識、AI Chat、音声合成を提供しているサーバにリクエストを順番に出していきます。
start.jsconst TextResponse = require(HELPER_BASE + 'textresponse'); exports.handler = async (event, context, callback) => { var body = JSON.parse(event.body); console.log(body); if( !body.message ) throw 'message is not set'; var wav = Buffer.from(body.message, 'base64'); // 音声の正規化+16ビット化 var norm = normalize_wave8(wav); // 音声認識 var ret = await speech_recognize(norm); console.log(ret); if( ret.length < 1 ) throw 'recognition failed'; // AI Chat var ret2 = await speech_talk(ret[0]); console.log(ret2); // 音声合成 var ret3 = await speech_to_wave(ret2); console.log(ret3); // 16ビットから8ビットに変換 var res = speech_wave16_to_wave8(ret3); console.log(res); return new TextResponse("text/plain", res.toString('base64')); // return new TextResponse("text/plain", body.message); // echoback };ユーティリティです。
textresponse.jsclass TextResponse{ constructor(content_type, context){ this.statusCode = 200; this.headers = {'Access-Control-Allow-Origin' : '*', 'Cache-Control' : 'no-cache', 'Content-Type': content_type }; if( context ) this.set_body(context); else this.body = ""; } set_error(error){ this.body = JSON.stringify({"err": error}); return this; } set_body(content){ this.body = content; return this; } get_body(){ return content; } } module.exports = TextResponse;その前後で、WAVEデータの整形を行っています。
<音声の正規化+16ビット化>
正規化は、音声が大きすぎたり、小さすぎたりしている場合に、適当なレベルに合わせることです。また、M5StickCで録音したWAVEデータは、中央値が0(unsigned 8bitの場合は128)ではなく少しずれているので、それの補正をします。ですが、GoogleのSpeech-to-Textは優れモノなので、特に正規化しなくても大丈夫です。
正規化と一緒に、WAVEデータの16ビット化をしています。こちらが本当に必要な作業です。
M5StickCから送られるWAVEデータは、データ量削減の意味もあって、モノラル1サンプリング8ビット長です。ですが、GoogleのSpeech-to-Textは、モノラル16ビット長を期待しているので、その変換を行う必要があります。start.jsfunction normalize_wave8(wav, out_bitlen = 16){ var sum = 0; var max = 0; var min = 256; for( var i = 0 ; i < wav.length ; i++ ){ var val = wav[i]; if( val > max ) max = val; if( val < min ) min = val; sum += val; } var average = sum / wav.length; var amplitude = Math.max(max - average, average - min); if( out_bitlen == 8 ){ const norm = Buffer.alloc(wav.length); for( var i = 0 ; i < wav.length ; i++ ){ var value = (wav[i] - average) / amplitude * (127 * 0.8) + 128; norm[i] = Math.floor(value); } return norm; }else{ const norm = Buffer.alloc(wav.length * 2); for( var i = 0 ; i < wav.length ; i++ ){ var value = (wav[i] - average) / amplitude * (32767 * 0.8); norm.writeInt16LE(Math.floor(value), i * 2); } return norm; } }<16ビットから8ビットに変換>
音声合成であるAWS Amazon Pollyからのレスポンスは、モノラル16ビット(signed)です。M5StickC側が期待するのは、モノラル8ビット(unsigned)です。その変換するための関数が以下です。
start.jsfunction speech_wave16_to_wave8(wav){ var buffer = Buffer.alloc(wav.length / 2); for( var i = 0 ; i < buffer.length ; i++ ){ buffer[i] = Math.floor(wav.readInt16LE(i * 2) / 256 + 128); } return buffer; }中継サーバでの音声認識の呼び出し
音声認識には、Google Cloud Speech APIのSpeech-to-Textを使っています。「OK Google」でも有名ですが、機械学習で賢くなっていることを期待して使わせていただいています。
呼び出す前に、準備が必要です。
GCPのコンソールから、「APIとサービス」→「APIライブラリ」を選択します。
そこから、Cloud Speech-to-Text APIを選択し、「有効にする」を押下します。次に、またGCPのコンソールから、「IAMと管理」→「サービスアカウント」を選択します。
そこで、「サービスアカウントとの作成」を押下し、適当なサービスアカウント名を入力して、最後にキーを作成します。形式はJSONにします。
そうすると、プロジェクト名-XXXXX-XXXXXXXXXXXX.json
のようなファイルが作られます。(本番では、権限を絞った方が良いです)あとは、そのファイルをSwagger-nodeの適当な場所において、.envにその場所とファイル名を記載しておきます。(環境変数への設定でもよいですが、dotenvが楽なので。。。)
GOOGLE_APPLICATION_CREDENTIALS=【ファイルのパス】もう一つの準備として、Speech-to-Textの呼び出しのため、Googleが提供しているnpmモジュールをインストールしておきます。
npm install @google-cloud/speech
これで準備ができました。あとは、以下のように実装します。
start.jsconst speech = require('@google-cloud/speech'); const client = new speech.SpeechClient(); async function speech_recognize(wav){ const config = { encoding: 'LINEAR16', sampleRateHertz: 8192, languageCode: 'ja-JP', }; const audio = { content: wav.toString('base64') }; const request = { config: config, audio: audio, }; return client.recognize(request) .then(response =>{ const transcription = []; for( var i = 0 ; i < response[0].results.length ; i++ ) transcription.push(response[0].results[i].alternatives[0].transcript); return transcription; }); }認識結果が複数返ってきますので、それを配列にして返してあげる関数です。(ですが、結局先頭の結果しか使っていませんが)
中継サーバでのAI Chatの呼び出し
AI Chatには、ユーザローカル社の人工知能チャットボット(chatbot)を使わせていただきました。以下から個人開発者向けのボットAPI利用申請をしていないようでしたら、申請しておきます。
人工知能チャットボット(chatbot):ユーザローカル
https://ai.userlocal.jp/document/free/top/そうすると、メールでAPIキーが払い出されます。
あとは、以下のように呼ぶだけです。
npmモジュールのnode-fetchを使っています。npm install node-fetch
start.jsconst fetch = require('node-fetch'); const USERLOCAL_API_KEY = '【ユーザローカルのAPIキー】'; async function speech_talk(message){ var body = { message: message, key: USERLOCAL_API_KEY, }; return do_post('https://chatbot-api.userlocal.jp/api/chat', body) .then(json =>{ return json.result; }); } function do_post(url, body){ return fetch(url, { method : 'POST', body : JSON.stringify(body), headers: { "Content-Type" : "application/json; charset=utf-8" } }) .then((response) => { if(!response.ok) throw "status is not 200."; return response.json(); }); }中継サーバでの音声合成の呼び出し
音声合成は、AWS Amazon Polly を使っています。
npmモジュールのaws-sdkを使っています。aws configureは実行しておきましょう。(詳細は省略!)
start.jsasync function speech_to_wave(message){ const pollyParams = { OutputFormat: 'pcm', // 音声フォーマット Text: message, VoiceId: 'Mizuki', TextType: 'text', SampleRate : '8000', }; return new Promise((resolve, reject) =>{ polly.synthesizeSpeech(pollyParams, (err, data) =>{ if( err ){ console.log(err); return reject(err); } var buffer = Buffer.from(data.AudioStream); return resolve(buffer); }); }); }声の種類を変えられますが、今回はMizukiさんを選択しています。
出力フォーマットは、mp3とかも選べるのですが、後続処理でサンプリングデータを加工するので、pcmにしておきます。pcmを選択すると、モノラル16ビット(signed)となります。サンプリングレートは、8KHzです。中継サーバの起動
一応これで、中継サーバの準備は整ったはずです。
GitHubに上がっているファイルの起動方法は以下の通りです。unzip m5stickc_chat.zip
cd m5stickc_chat
npm install
node app.jsこれで、ポート10080で待ち受けているはずです。
M5StickCへの書き込み
Arduinoで書き込みます。
最終的なソースはこんな感じです。
#include <M5StickC.h> #include <driver/i2s.h> #include <WiFi.h> #include <HTTPClient.h> #include <base64.hpp> HTTPClient http; const char *host = "【中継サーバのURL】/speech"; const char* wifi_ssid = "【WiFiアクセスポイントのSSID】"; const char* wifi_password = "【WiFiアクセスポイントのパスワード】"; #define htonl(x) ( ((x)<<24 & 0xFF000000UL) | \ ((x)<< 8 & 0x00FF0000UL) | \ ((x)>> 8 & 0x0000FF00UL) | \ ((x)>>24 & 0x000000FFUL) ) #define PIN_CLK (0) // I2S Clock PIN #define PIN_DATA (34) // I2S Data PIN #define SAMPLING_RATE (8192) // サンプリングレート(44100, 22050, 16384, more...) #define BUFFER_LEN (1024) // バッファサイズ #define SAMPLEING_SEC (4) // 最大サンプリング時間(秒) #define STORAGE_LEN (SAMPLING_RATE * SAMPLEING_SEC) // 本体保存容量 #define WAVE_EXPORT (0) // WAVEファイルに出力するか #define BLANK_LINE " " uint8_t soundBuffer[BUFFER_LEN]; // DMA転送バッファ uint8_t soundStorage[STORAGE_LEN]; // サウンドデータ保存領域 char httpBuffer[(STORAGE_LEN + 2)/ 3 * 4]; bool recFlag = false; // 録音状態 int recPos = 0; // 録音の長さ int http_post(uint8_t *p_inout_buffer, int in_length); // 再生をする void i2sPlay(){ // 再生設定 i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN), .sample_rate = SAMPLING_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 2, .dma_buf_len = BUFFER_LEN, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0, }; // 再生設定実施 i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, NULL); i2s_zero_dma_buffer(I2S_NUM_0); // 再生 size_t transBytes; size_t playPos = 0; while( playPos < recPos ){ for( int i = 0 ; i < BUFFER_LEN ; i+=2 ){ soundBuffer[i] = 0; // 下位8ビットは無視される soundBuffer[i+1] = soundStorage[playPos]; // 上位8ビットにuint8_tのデータを入れる playPos++; } // データ転送 i2s_write(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS)); } // 後始末 i2s_zero_dma_buffer(I2S_NUM_0); i2s_driver_uninstall(I2S_NUM_0); } // 録音をする void i2sRecord() { // 録音用設定 i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM), .sample_rate = SAMPLING_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ALL_RIGHT, .communication_format = I2S_COMM_FORMAT_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 2, .dma_buf_len = BUFFER_LEN, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0, }; // PIN設定 i2s_pin_config_t pin_config; pin_config.bck_io_num = I2S_PIN_NO_CHANGE; pin_config.ws_io_num = PIN_CLK; pin_config.data_out_num = I2S_PIN_NO_CHANGE; pin_config.data_in_num = PIN_DATA; // 録音設定実施 i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, &pin_config); i2s_set_clk(I2S_NUM_0, SAMPLING_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO); // 録音開始 recFlag = true; xTaskCreatePinnedToCore(i2sRecordTask, "i2sRecordTask", 2048, NULL, 1, NULL, 1); } // 録音用タスク void i2sRecordTask(void* arg) { // 初期化 recPos = 0; memset(soundStorage, 0, sizeof(soundStorage)); vTaskDelay(500); //delay(portMAX_DELAY); // LED On digitalWrite(GPIO_NUM_10, LOW ); // 録音処理 while (recFlag) { size_t transBytes; // I2Sからデータ取得 i2s_read(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS)); // int16_t(12bit精度)をuint8_tに変換 for (int i = 0 ; i < transBytes ; i += 2 ) { if ( recPos < STORAGE_LEN ) { int16_t* val = (int16_t*)&soundBuffer[i]; soundStorage[recPos] = ( *val + 32768 ) / 256; recPos++; if( recPos >= sizeof(soundStorage) ){ recFlag = false; break; } } } Serial.printf("transBytes=%d, recPos=%d\n", transBytes, recPos); vTaskDelay(1 / portTICK_RATE_MS); } // LED Off digitalWrite(GPIO_NUM_10, HIGH); i2s_driver_uninstall(I2S_NUM_0); if( recPos > 0 ){ int ret = http_post(soundStorage, recPos); if( ret > 0 ){ recPos = ret; i2sPlay(); } } // タスク削除 vTaskDelete(NULL); } int http_post(uint8_t *p_inout_buffer, int in_length){ strcpy(httpBuffer, "{\"message\": \""); encode_base64(p_inout_buffer, in_length, (unsigned char*)&httpBuffer[strlen(httpBuffer)]); strcat((char*)httpBuffer, "\"}"); Serial.println("HTTP Post"); http.begin(host); http.addHeader("Content-Type", "application/json"); int status_code = http.POST((uint8_t*)httpBuffer, strlen(httpBuffer)); if( status_code != HTTP_CODE_OK ){ Serial.println("Status is not 200"); http.end(); return -1; } int len = http.getSize(); WiFiClient * stream = http.getStreamPtr(); int ptr = 0; while(http.connected() && (len > 0 || len == -1)) { size_t size = stream->available(); if(size) { if(size > (sizeof(httpBuffer) - ptr) ){ Serial.println("receive overflow"); http.end(); return -1; } int c = stream->readBytes(&httpBuffer[ptr], size); ptr += c; if(len > 0) len -= c; } delay(1); } httpBuffer[ptr] = '\0'; http.end(); return decode_base64((unsigned char*)httpBuffer, p_inout_buffer); } void wifi_connect(void){ Serial.print("WiFi Connenting"); WiFi.begin(wifi_ssid, wifi_password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.print("."); } Serial.println(""); Serial.print("Connected : "); Serial.println(WiFi.localIP()); } void setup() { M5.begin(); M5.Lcd.setRotation(3); M5.Lcd.fillScreen(BLACK); M5.Lcd.setTextColor(WHITE, BLACK); M5.Lcd.println("[M5StickC]"); pinMode(GPIO_NUM_10, OUTPUT); digitalWrite(GPIO_NUM_10, HIGH); i2sPlay(); wifi_connect(); M5.Lcd.println("Sound Recorder"); M5.Lcd.println("BtnA Record"); M5.Lcd.println("BtnB Play"); } void loop() { M5.update(); if ( M5.BtnA.wasPressed() ) { // 録音スタート M5.Lcd.setCursor(0, 36); M5.Lcd.println("REC..."); Serial.println("Record Start"); i2sRecord(); } else if ( M5.BtnA.wasReleased() ) { // 録音ストップ M5.Lcd.setCursor(0, 36); M5.Lcd.println(BLANK_LINE); recFlag = false; delay(100); // 録音終了まで待つ Serial.println("Record Stop"); // WAVEファイルをシリアルに出力 if ( WAVE_EXPORT ) { Serial.printf("52494646"); // RIFFヘッダ Serial.printf("%08lx", htonl(recPos + 44 - 8)); // 総データサイズ+44(チャンクサイズ)-8(ヘッダサイズ) Serial.printf("57415645"); // WAVEヘッダ Serial.printf("666D7420"); // フォーマットチャンク Serial.printf("10000000"); // フォーマットサイズ Serial.printf("0100"); // フォーマットコード Serial.printf("0100"); // チャンネル数 Serial.printf("%08lx", htonl(SAMPLING_RATE)); // サンプリングレート Serial.printf("%08lx", htonl(SAMPLING_RATE)); // バイト/秒 Serial.printf("0100"); // ブロック境界 Serial.printf("0800"); // ビット/サンプル Serial.printf("64617461"); // dataチャンク Serial.printf("%08lx", htonl(recPos)); // 総データサイズ for (int n = 0; n <= recPos; n++) { Serial.printf("%02x", soundStorage[n]); } Serial.printf("\n"); } } else if ( M5.BtnB.wasReleased() ) { // 再生スタート M5.Lcd.setCursor(0, 36); M5.Lcd.println("Play..."); Serial.println("Play Start"); i2sPlay(); M5.Lcd.setCursor(0, 36); M5.Lcd.println(BLANK_LINE); Serial.println("Play Stop"); } delay(10); }以下の部分を環境に合わせて変更してください。
const char *host = "【中継サーバのURL】/speech"; const char* wifi_ssid = "WiFiアクセスポイントのSSID"; const char* wifi_password = "【WiFiアクセスポイントのパスワード】";M5StickCの使い方
Aボタンを押したまま、チャットしたい言葉を話します。LEDが点灯しますのでわかるかと思います。最大4秒間です。
そうすると勝手に中継サーバに録音データをアップして、チャットの結果が返ってきて、スピーカHATから再生されます。終わりに
・再生のサンプリングレートがあっていないような気がします。。。早口なんですよねえ。
・再生の品質がわるいです。PIN設定が間違っているのか、8ビット+8KHzの宿命なのか、これがスピーカ性能の限界なのか。。。
・WiFiが不安定です。もともとM5StickCのアンテナの性能は高くないです。
・全体的に不安定だなあ。以上
- 投稿日:2020-05-17T12:03:18+09:00
intellijは無料版だとsassのためのサポートしてないみたいだから、その設定やるよーーの記事 要はsassをインストールするための手順を書きたいのよ、自分用にね! ここのタイトル欄って結構文字数入れられるんだね
はじめに
spring boot でアプリを作っていて、いざ見た目を作ろうとした時に、sassを使いたい!ってなった。
でも今使っているIDEはintellijでそれは無料版だとsassはサポートされてないっぽい。
だからsass入れていく!とっても簡単でした。node.jsのインストール
以下を上から実行していく。
$ brew install nodebrew $ nodebrew -v "ここでインストールを確認" $ mkdir -p ~/.nodebrew/src $ nodebrew install-binary stable $ nodebrew ls $ nodebrew use v.14.2.0 "1つ上のコマンで表示されたversionを入力" $ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash-profile "ターミナルを再起動" $ node -v "version表示されたら成功"sassのインストール
以下を上から実行
$ npm install -g sass $ sass --version "ここでインストールを確認"ここまでで、下準備は完了
scssファイルを作成して、コンパイル
コンパイルしたいscssファイルを作成し、そのファイルがあるところまで移動したのち以下のコマンドでコンパイルする。
今回はstyle.scss
をstyle.css
にコンパイルしたいとする。$ sass style.scss:stylecss
これで、同じディレクトリにコンパイルされたcssファイルが出来上がる。
自動コンパイルの設定
cssを変更する度、手動でコンパイルするのは面倒臭い。
自動コンパイルも超簡単。以下のコマンドを打ち込むだけ。$ sass --watch style.scss:style.css超簡単!
- 投稿日:2020-05-17T11:20:16+09:00
[javascript]初心者が関数・コールバック関数についてをまとめてみた
javascriptで関数の理解が難しいと感じたので、アウトプットします。
僕と同じようなプログラミング初心者の方にお役立ちできれば幸いです。関数とは
関数とは、「ある値を与えると、別の値を返す機能」
ある値を「引数(ひきすう)」と呼び、別の値を「戻り値」と呼びます。言い換えると、関数は「引数を与えると、戻り値を返す機能」をさします。
下記の図が参考になります。実際にコードを書いてきます。
関数
function.js//関数 const 定数名 = function(){ //まとめたい処理や機能 };普通の関数式の形
アロー関数
function.js//アロー関数 const 定数名 = () => { //まとめたい処理や機能 };ES6から導入された形式で1つ目の形式と同じ処理を行なっています。
例
function.jsconst greet = () => { console.log("こんにちは!"); };この場合、
greet();
を記載することで関数greetの「”こんにちは!”」を呼び出すことができます。引数&戻り値
function.jsconst kakezan = (a,b) => { return a*b; }; const sum = kakezan(3,2);掛け算の関数を作りました。
呼び出し元で3と2を設定し、引数を与えます。
a*b
の掛け算処理行なっているので、6
が戻り値になります。コールバック関数
関数の引数に渡された関数をコールバック関数という。
コールバック関数例
function.js// コールバック関数を実行する関数 function execCallback(callback) { console.log('I call callback'); callback(); } // execCallback()に渡されるコールバック関数 var myCallback = function() { console.log('This is my callback'); } // execCallback()にコールバック関数を渡して実行する execCallback(myCallback); // => 'I call callback' // 'This is my callback'
callback();
のところで引数のコールバック関数を実行しています。参考:JavaScript中級者への道【5. コールバック関数】
非同期処理
「ある関数が呼び出されたとき、戻り値として本来渡したい結果を返すのではなく、一度関数としては終了し(=呼び出し元に戻る)、後で『本来渡したかった値』を返せる状態になったときに、呼び出し元にその値を通知する」という仕組み
参考:非同期処理ってどういうこと?JavaScriptで一から学ぶ
- 投稿日:2020-05-17T11:17:50+09:00
依存性を減らす~JavaScript界の事情
とあるプロジェクトで
yarn upgrade
を行ったところ、こんなメッセージが出てきました。warning (略) > chokidar@2.1.8: Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.そもそも、Chokidarって何?
Node.jsには、ファイル更新を管理する手法がいくつかありますが、OSなど環境によって挙動が異なってしまいます。
Chokidarは、それらの手法を一貫して管理できるようなラッパーです。
直接使っている人は多くないかも知れませんが、
sass
コンパイラやwebpack-dev-server
など、ビルドツール系のライブラリでは「ファイル監視」機能があると便利なので、よく使われています。依存性の削減
「Chokidar 3にすると依存パッケージが15分の1になる」という話について、作者が詳細を書いていました。
簡単にまとめますと、
- 第三者のパッケージを参照していると、不意に差し替えられたりする危険がある
- ネイティブエクステンションは不安定な
node-gyp
を捨てて、Node.jsネイティブに実装されたN-APIに乗り換え- パースに必要なサブライブラリも、依存性の多い別ライブラリを参照するのはやめて書き直し
- 結果、依存ライブラリ数は201個から15個に、ディスク容量は8MB超から500kB未満まで削減
Chokidar特有の事情
ただし、JavaScript界隈の状況を考えると、一般に敷衍するのが難しいことが考えられます。Chokidarの好条件としては、
- 関連するパッケージの開発支援を行うだけのパワーがある
- Node.js専用(ブラウザで動かすことは、そもそも不可能)
ということがあります。資金的、プログラミングリソース的に手が回らなければ、「全く第三者のパッケージを排除する」という選択は行なえません。
そして、Node.js専用の場合、ブラウザで使えるライブラリを書くのと比べて以下のようなメリットがあります。
- 動くランタイムのバージョン範囲を決めることが可能
- ブラウザバンドルほどにはコード容量への制約が厳しくない
ブラウザの場合、処理系は閲覧者のものなので、ある程度古いバージョンを考慮せざるを得ず、最新版のJavaScriptにある機能を積極的に使う、というわけにもいかなくなります。
そして、HTMLに適用するJavaScriptはネットワーク経由で送信されますので、容量を減らす方向への圧力も強いです。依存性を回避するためとはいえ、同じコードを何度も書いて容量を消費するようなことは、ブラウザバンドルだと嫌われてしまいます。
関連記事
- 投稿日:2020-05-17T10:23:14+09:00
Next.jsでsessionとPassport.js(認証)を使う
Next.jsでsessionとPassport.jsを使う
Next.jsでsessionとかPassport.js(認証)を使う方法を書いてみる
next.js session
とかnext.js 認証
とかでググるとNextにexpressを入れる感じでのサンプルは結構あったけど、デフォルトで用意されているAPI Routeを利用したやつの実装のサンプルがあまり見つけれなかったので書いてみるNext.js では connect というmiddlewareレイヤーに対応していて、これに対応しているライブラリなどであれば利用することが出来ます
サンプルは下記の配置で書いていきます
- middleweres
- index.ts
- connect.ts
- passport.ts
- session.ts
- page
- api
- userInfo.ts
- login.ts
- logout.ts
利用しているライブラリ
- express-session (expressとか書いてるけど問題なくつかえる
- connect-redis (今回のサンプルではsessionをredisで管理するようにしているので
- passport (認証
- passport-local (認証を独自ロジックで行いたいので
yarn add -D express-session connect-redis passport passport-local
Middlewareの前準備
connectするための関数を定義
connect.tsimport { NextApiRequest, NextApiResponse } from 'next' export const connectMiddleware = (req: NextApiRequest, res: NextApiResponse, middleware: Function) => { return new Promise((resolve, reject) => { middleware(req, res, (result: any) => { if (result instanceof Error) { return reject(result) } return resolve(result) }) }) }https://nextjs.org/docs/api-routes/api-middlewares#connectexpress-middleware-support
これを関数化しただけsessionを定義
session.tsimport { NextApiRequest, NextApiResponse } from 'next' import session from 'express-session' import { createClient } from 'redis' import { connectMiddleware } from './connect' const RedisStore = require('connect-redis')(session) export type Session = { session: { [key: string]: any } } // このへんよしなに。開発用の設定になってる const config = { saveUninitialized: true, secret: 'keyboard cat', resave: false, store: new RedisStore({ client: createClient({ host: '0.0.0.0', port: 6380, prefix: 'backend:', }), }), cookie: { httpOnly: true, sameSite: true, secure: false, }, } export const withSession = async (req: NextApiRequest, res: NextApiResponse) => { await connectMiddleware(req, res, session(config)) }Passport.jsを定義
passport.tsimport { NextApiRequest, NextApiResponse } from 'next' import { connectMiddleware } from './connect' const passport = require('passport') const LocalStrategy = require('passport-local').Strategy // passport.d.ts の上書きがめんどいのでこれで(さぼり) export type PassportFunctions = { authInfo?: any user?: User login(user: {id: number, name: string}, done: (err: any) => void): void login(user: {id: number, name: string}, options: any, done: (err: any) => void): void logIn(user: {id: number, name: string}, done: (err: any) => void): void logIn(user: {id: number, name: string}, options: any, done: (err: any) => void): void logout(): void logOut(): void isAuthenticated(): boolean isUnauthenticated(): boolean } passport.use( new LocalStrategy( { usernameField: 'id', passwordField: 'passport', }, async (username: string, password: string, done: Function) => { // なんか認証してユーザーを取得するやつ const user: {id: number, user: string} | null = await authUser({username, password}) // 取得エラー if (!result) { done(null, false) return } // IDと名前 done(null, user) } ) ) passport.serializeUser((user: User, done: Function) => { done(null, user) }) passport.deserializeUser((user: User, done: Function) => { done(null, user) }) export const withPassport = async (req: NextApiRequest, res: NextApiResponse) => { await connectMiddleware(req, res, passport.initialize()) // Passport.jsでセッションを使いたいので await connectMiddleware(req, res, passport.session()) }Passport.js(認証) & sessionを使うための高階関数を定義
index.tsimport { NextApiRequest, NextApiResponse } from 'next' import { Session, withSession } from './session' import { PassportFunctions, withPassport } from './passport' // デフォルトのNextApiRequestとライブラリで拡張されたのをくっつけて再定義 type Request = NextApiRequest & Session & PassportFunctions type Response = NextApiResponse & Session type Options = { requiredAuth: boolean // 認証がされているかどうか、されていなければ403を返す } export const withApiMiddlewares = (options: Options) => (fn: (req: Request, res: Response) => void) => { return async (req: Request, res: Response) => { await withSession(req, res) await withPassport(req, res) // 認証していなければ403を返す if (options.requiredAuth && !req.isAuthenticated()) { res.status(403).json({ message: 'Forbidden' }) return } fn(req, res) } }あとは withApiMiddlewares を使うことでAPIの中でsessionやPassport.js(認証)を使うことが可能になった
withApiMiddlewares の第一引数はライブラリのオプションなどで利用。第2引数はメインの処理を書く感じ下記が例
export default withApiMiddlewares({ requiredAuth: true, methods: ['GET', 'POST'] })(async (req, res) => { res.status(200).json({ message: 'ok' }) })こんな感じで使える。第一引数のmethodsみたいな感じで利用できるHttpMethodを限定するみたいな拡張をしてもいいかも
高階関数にすることで、req resの型定義も勝手にされるのでサボれるミドルウェアを使ったAPIの例
ユーザーの情報を返すAPI
page/api/userInfo.tsimport { withApiMiddlewares } from '../../middlewares' export default withApiMiddlewares({ requiredAuth: true })(async (req, res) => { res.status(200).json({ user: req.user, }) })ログインのやつ
page/api/loggin.tsimport passport from 'passport' import { withApiMiddlewares } from '/middlewares' import { connectMiddleware } from '/middlewares/connect' export default withApiMiddlewares({ requiredAuth: false })(async (req, res) => { await connectMiddleware( req, res, passport.authenticate('local', (err: any, user: {id: number, name: string} | null) => { if (err || !user) { res.writeHead(302, { Location: '/' }).end() return } req.logIn(user, (error: any) => { if (error) { res.writeHead(302, { Location: '/' }).end() return } res.writeHead(302, { Location: '/loggedInPage' }).end() }) }) ) })ログアウトのやつ
page/api/logout.tsimport { withApiMiddlewares } from '../../middlewares' export default withApiMiddlewares({ requiredAuth: false })(async (req, res) => { req.logout() res.status(200).end() })結構コードがメインの記事になりましたが、これで利用可能です
next-connect というライブラリもあり、これを使うのもありかも。
Next.jsのサンプルでも使われてたりはします https://github.com/zeit/next.js/tree/canary/examples以上。ご査収のほどよろしくお願い致します
- 投稿日:2020-05-17T02:14:12+09:00
爆速!Vercelとfreenomで独自ドメインのサイトを無料で作成する
無料でWebアプリケーションのホスティングが出来るnow.shが名称変更してVercelになってましたね。
使い勝手などがどうなのか触って試してみました。
結果、ただ触るだけだと簡単すぎて記事にならないので、ついでにfreenomも使って無料で独自ドメイン(カスタムドメイン)も反映させてみます。
※タイトルの爆速!は処理速度ではなく手順の話
作るもの
こんな感じの独自ドメインのWebサイトを作成(デプロイ)します。
ちゃんとSSLも対応してます。
Vercel
読み方はバーセル?ですかね。
無料でWebアプリケーションのデプロイが出来ます。
コマンド一発で出来ます
Vercelで爆速デプロイ
- 1. 簡単なhtmlファイルを作成します。
20200516sample
というフォルダを作成してindex.htmlを作成しました。以下の記事のhtmlをコピーして作りました。
- 2. Vercelのコマンドをインストール
npm i -g vercel
- 3. デプロイ
vercel以上です。相変わらずシンプルで簡単。
(ここで初めての人はメールアドレスの認証などを聞かれるかもしれません。僕の場合はnow(旧名称)で使っていたからだと思いますが特に聞かれませんでした。)あとは対話式の質問が出てくるので答えていけばデプロイされます。
プロジェクト名を聞かれたのでsuikousaibaiにしました。なんとなく。
https://suikousaibai.n0bisuke.now.sh のURLでデプロイされました。
8888。
ここまででもだいぶ便利です。
ついでに独自ドメインもやってみます。
FreenomとVercelで独自ドメインなWebサイト
無料でドメインを取得できるサービスです。
https://www.freenom.com/ja/index.html
- 1. Freenomでドメインを取得します。
ここは割愛します。無料なので空いているものを取ってみましょう。
suikousaibai.gq
というドメインを取得してみました。
- 2. Vercelのネームサーバー情報を取得
Vercelの管理画面にいきます。
https://vercel.com/dashboard/domains
add
ボタンを押します。作成したアプリ(ここでは
suikousaibai
)を選択して進みます。freenomで取得したドメイン(ここでは
suikousaibai.gq
)を入力して進みます。次の画面で
Checking Domain Status
の箇所が読み込み中...のような表示になります。 画面表示が変わるので少し待ちましょう。しばらく待つと
Intended Nameservers
のネームサーバー情報が表示されます。
ここの情報はユーザーやアプリごとで若干異なるみたいです。今回は以下のような情報でした。
a.zeit-world.co.uk c.zeit-world.org d.zeit-world.net f.zeit-world.com
- 3. 取得したドメインをVercelのアプリに紐付け
このネームサーバーの情報をFreenom側に設定します。
Freenomのドメイン管理画面から利用するドメインの設定(Manage Domain)のボタンを押してドメインの設定画面に移動します。
Management Tools
>Nameservers
からネームサーバーを入力して保存します。これで設定は完了です。
- 4. 浸透されるのを待つ
設定は完了ですが、反映されるのに時間がかかるので少し待ちましょう。
数分〜数十分かかるときもありそうな印象です。気長に待ちましょう。完了すると冒頭で紹介したような独自ドメインのWebサイトが出来ます。
お疲れ様でした。
更新するとき
index.htmlを編集してvercelコマンドで更新できます。
所感
無料でやれるのはやはり気軽で良いですね。
Freenom側のキャッシュなのか、更新時も反映されるのに時間がかかる印象でした。無料なので文句言えないところはあるねぇくらいの感覚で使うと良いのかなと思います。