20200517のNode.jsに関する記事は11件です。

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.js

Arduino

serialCom.ino
int 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.js
var 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.js
const 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へアクセスするとこんな感じのグラフが表示されます。
ダウンロード.png
光センサーの上で手で光を遮ったりしたグラフになります。光を遮ると光センサーの抵抗値が上がるので電圧も上がります。
横軸がなぜか均等にならないのですが、気が向いたら改善します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】記念日を通知するLINE botをheroku + Node.jsで作る

概要

この記事では、HerokuとNode.jsを活用して特定のユーザ(少人数を想定)に記念日を通知するLINE Botを作るノウハウを紹介します。ここでの通知とは、時間指定でbotからユーザへの簡単なテキストメッセージを送ることを指します。

事前準備1 LINE bot のチャネルの用意

LINE Botを作成するためにはLINEのMessaging APIというサービスを使います。
まずこのサービスを利用するためのチャネルを作成します。
以下をページを参考に必要な情報を入力して進めてください。

https://developers.line.biz/ja/docs/messaging-api/getting-started/#%E3%83%81%E3%83%A3%E3%83%8D%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90

実在するアカウントと同様にアイコンも設定できます。
ここでは、某作品より以下のようなアイコンを設定しました。 
WS000006.JPG

Messaging APIには、botからユーザへのメッセージ(Push)とユーザがbotに送ったメッセージに対する返信(Reply)
がありますが、今回使用するのは、Pushの方です。
messaging-api-thumb0.png

https://developers.line.biz/ja/services/messaging-api/
より引用

事前準備2 ホストするサーバー(Heroku)の用意

次に、botをホストするサーバを用意します。
今回は無料で多くのサービスが利用可能なHerokuを使います。
Herokuの基本的な使い方やデプロイするまでの流れは、以下の記事が参考になると思います。

https://qiita.com/arashida/items/b2f2e01259238235e187
https://qiita.com/shti_f/items/b4b5d830672d908eff4e

botの開発

今回はbotからのメッセージを送ることを想定しているので、起点はHerokuのアドオン機能であるheroku schedulerを利用することにします。(詳細は後述)
この設定は後ほど行うとして、まずは通知するためのプログウラムを記載します。
通常使うindex.jsとは別にファイルを一つ作成します。

https://qiita.com/nkjm/items/38808bbc97d6927837cd

コードを書くにあたっては、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のアドオン機能で日時や毎時の実行であれば、この機能で十分です。
※細かい設定はできないので、そのあたりまで実装したい方は別のサービスを使ったほうがいいでしょう
実際の設定は以下の記事を参考にさせていただきました。

https://qiita.com/Taro_man/items/2eab3e3acad88c5b759e

実際の設定画面は以下の通りです。
npm run 「package.jsonで指定したjob」という記載の仕方です。
※時間は標準時での設定しかできない点に要注意。
WS000007.JPG

設定は以上です!
実際に指定したユーザのラインにメッセージが飛んでいることを確認できましたでしょうか?

まとめ

ラインは生活の中に浸透していることもあって、アイディアと工夫次第で面白いことができそうですね。
これからも可能性を探っていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.png

skill-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の東京リージョン上に反映され、
日本語化されたスキルを利用できるようになるはずです。

・Alexa Developer Console
alexa.png

・AWS
lambda.png

あとはどんどん書き換えていけばOKです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカルパッケージをシンボリックリンクでインストールしたら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.jsonyarn.lockの内容を確認しましょう。yarnでの手法についてはこちらが参考になります。

なお、関連する話題のissueがありますので併せて確認すると良いと思います。


  1. npmの場合file:path/to/directoryですが、yarnの場合file:path/to/directoryです。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deno で遊んでみよう

Image

deno とは

deno は Node.js の製作者 Ryan Dahl 氏により開発された TypeScript を標準でサポートするランタイムです。
Node.js のときに得た教訓をもとに設計がされていますが、Node.js と互換性のある実装というわけではありません。
JavaScript のエンジン自体は Node.js と同じく V8 を利用しています。
公式サイトはこちら

2020-05-13 にめでたく v1.0 がリリースされたので今回ちょっとだけ遊んでみることにします。
https://deno.land/v1

Node.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 ですが直接ブラウザで開くとこのように中身を確認できます。
Image

あっけなく終わってしまいましたが deno のスクリプトを実行する事ができました。
ここまででは Node.js をインストールして サンプルスクリプトを実行したのとあまり大差ないのでもう少し遊んでみましょう。

deno をより体感する

続いても公式サイトにあるサンプルスクリプトを利用します。(自分好みにスタイルを変更しています)
下記のファイルをダウンロードするか写経して server.ts として保存しましょう。

server
import { 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 Pagespika.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 でやったほうが手軽さがあるのではないかな思ったりしました。

from Scrapbox ?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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と会話をしてみたいと思います。

全体的な流れは以下の通りです。

image.png

音声認識には、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_chat

M5StickCでの録音・再生

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.js
const 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.js
class 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.js
function 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.js
function 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を選択し、「有効にする」を押下します。

image.png

次に、またGCPのコンソールから、「IAMと管理」→「サービスアカウント」を選択します。

image.png

そこで、「サービスアカウントとの作成」を押下し、適当なサービスアカウント名を入力して、最後にキーを作成します。形式はJSONにします。
そうすると、プロジェクト名-XXXXX-XXXXXXXXXXXX.json のようなファイルが作られます。(本番では、権限を絞った方が良いです)

あとは、そのファイルをSwagger-nodeの適当な場所において、.envにその場所とファイル名を記載しておきます。(環境変数への設定でもよいですが、dotenvが楽なので。。。)

GOOGLE_APPLICATION_CREDENTIALS=【ファイルのパス】

もう一つの準備として、Speech-to-Textの呼び出しのため、Googleが提供しているnpmモジュールをインストールしておきます。

npm install @google-cloud/speech

これで準備ができました。あとは、以下のように実装します。

start.js
const 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/

image.png

そうすると、メールでAPIキーが払い出されます。

あとは、以下のように呼ぶだけです。
npmモジュールのnode-fetchを使っています。

npm install node-fetch

start.js
const 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.js
async 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のアンテナの性能は高くないです。
・全体的に不安定だなあ。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.scssstyle.cssにコンパイルしたいとする。

$ sass style.scss:stylecss

これで、同じディレクトリにコンパイルされたcssファイルが出来上がる。

自動コンパイルの設定

cssを変更する度、手動でコンパイルするのは面倒臭い。
自動コンパイルも超簡単。以下のコマンドを打ち込むだけ。

$ sass --watch style.scss:style.css

超簡単!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[javascript]初心者が関数・コールバック関数についてをまとめてみた

javascriptで関数の理解が難しいと感じたので、アウトプットします。
僕と同じようなプログラミング初心者の方にお役立ちできれば幸いです。

関数とは

関数とは、「ある値を与えると、別の値を返す機能」
ある値を「引数(ひきすう)」と呼び、別の値を「戻り値」と呼びます。

言い換えると、関数は「引数を与えると、戻り値を返す機能」をさします。
下記の図が参考になります。

(参考:wikipedia)
image.png

実際にコードを書いてきます。

関数

function.js
//関数
const 定数名 = function(){
  //まとめたい処理や機能
};

普通の関数式の形

アロー関数

function.js
//アロー関数
const 定数名 = () => {
  //まとめたい処理や機能
};

ES6から導入された形式で1つ目の形式と同じ処理を行なっています。

function.js
const greet = () => {
  console.log("こんにちは!");
};

この場合、greet();を記載することで関数greetの「”こんにちは!”」を呼び出すことができます。

引数&戻り値

function.js
const 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で一から学ぶ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

依存性を減らす~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はネットワーク経由で送信されますので、容量を減らす方向への圧力も強いです。依存性を回避するためとはいえ、同じコードを何度も書いて容量を消費するようなことは、ブラウザバンドルだと嫌われてしまいます。

関連記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { 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.ts
import { withApiMiddlewares } from '../../middlewares'

export default withApiMiddlewares({ requiredAuth: true })(async (req, res) => {
  res.status(200).json({
    user: req.user,
  })
})

ログインのやつ

page/api/loggin.ts
import 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.ts
import { 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

以上。ご査収のほどよろしくお願い致します

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

爆速!Vercelとfreenomで独自ドメインのサイトを無料で作成する

無料でWebアプリケーションのホスティングが出来るnow.shが名称変更してVercelになってましたね。

使い勝手などがどうなのか触って試してみました。

結果、ただ触るだけだと簡単すぎて記事にならないので、ついでにfreenomも使って無料で独自ドメイン(カスタムドメイン)も反映させてみます。

※タイトルの爆速!は処理速度ではなく手順の話

作るもの

こんな感じの独自ドメインのWebサイトを作成(デプロイ)します。

https://suikousaibai.gq

ちゃんとSSLも対応してます。

Vercel

読み方はバーセル?ですかね。

https://vercel.com

無料でWebアプリケーションのデプロイが出来ます。

コマンド一発で出来ます

Vercelで爆速デプロイ

  • 1. 簡単なhtmlファイルを作成します。

20200516sampleというフォルダを作成してindex.htmlを作成しました。

以下の記事のhtmlをコピーして作りました。

Vue.jsで東京都の今日の天気を表示するWebサイトを作るコピペサンプル

  • 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側のキャッシュなのか、更新時も反映されるのに時間がかかる印象でした。

無料なので文句言えないところはあるねぇくらいの感覚で使うと良いのかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む