- 投稿日:2019-12-04T23:26:26+09:00
obnizと圧電スピーカーを使ってキーボードをキーボード(音楽)に変えてみた
はじめに
いまOTTOというArduinoベースのオープンソースロボットをobnizに移植する個人プロジェクトを進めています(IoTLT vol.54でLTしたやつです)。
OTTOには圧電スピーカーも搭載されており、これを使ってロボっぽいピロピロ音を出すことができます。先日、この圧電スピーカーを使ってobniz版OTTOに今流行りのパプリカを歌わせてみました。
OTTOにパプリカ歌って踊らせてみた pic.twitter.com/6W8Zq1ncjT
— 田中みそ@LINE API Expertになりました! (@miso_develop) November 23, 2019圧電スピーカーで音を鳴らすのは周波数を渡すだけととても単純だったので、これを応用してobnizと圧電スピーカーを使って普段コードを打ち込んでるキーボードをキーボード(音楽用)に変えてみたいと思います。
ハードウェア実装
まずは圧電スピーカーを用意します。
obnizのパーツライブラリページに秋月やSwitch Scienceの通販リンクがあります。
大きさとかで30円~100円と割と種類ありますが、なんでもいいと思います。
私は手元にあったやつを使いました。このうち左下のやつは飛び抜けて音がでかかったため除外しました。
他のは大体同じ音量ですが、左上のは音が悪かったです。そしてこれらをobnizに直挿しします。
ブレッドボードを使えば12個繋げられますが、obnizと言えばモーター然り直差しなので、このままいきます。ソフトウェア実装
キーボードのイベント拾うならHTMLで実装するのが手軽ですが、画面上にキーボードの画像表示してどこが押されてるかとかの描画もないといけない感じがするので、HTMLは使いません。
代わりにNode.jsで実装します。Node.jsでリアルタイムにキーイベントを拾うには↓のパッケージが良さそうです。
iohookこんな感じでキーイベントだけでなくマウスポインターとかも拾えます。
index.js'use strict'; const ioHook = require('iohook'); ioHook.on("mousemove", event => { console.log(event); // result: {type: 'mousemove',x: 700,y: 400} }); ioHook.on("keypress", event => { console.log(event); // result: {keychar: 'f', keycode: 19, rawcode: 15, type: 'keypress'} }); //Register and stark hook ioHook.start();あとは圧電スピーカーの制御コードをobnizのパーツライブラリからコピって、keydownで音鳴らしてkeyupで音止めるようにすればいいわけですね。
完成したコード
こちらが完成したコードです。
キーボードのキーコードとスピーカーで鳴らす周波数のマップオブジェクトを用意しています。
それと各圧電スピーカーの割当の制御とかも書いてます。
あとは上記で書いたような実装です。
npm install obniz iohook
したうえで、obniz IDと各スピーカーのピン番号を書き換えて実行してみてください。index.js"use strict" const ioHook = require("iohook") const Obniz = require("obniz") const keymap = { "16": 370, // F#3 "30": 392, // G3 "17": 415, // G#3 "31": 440, // A4 "18": 466, // A#4 "32": 494, // B4 "33": 523, // C4 "20": 554, // C#4 "34": 587, // D4 "21": 622, // D#4 "35": 659, // E4 "36": 698, // F4 "23": 740, // F#4 "37": 784, // G4 "24": 831, // G#4 "38": 880, // A5 "25": 932, // A#5 "13": 988, // B5 "39": 1047, // C5 "41": 1109, // C#5 "27": 1175, // D5 "26": 1245, // D#5 } const obniz = new Obniz("xxxx-xxxx") obniz.onconnect = async () => { const speakers = [] speakers.push( // スピーカーは繋げられるだけここに列挙 { assign: 0, obniz: obniz.wired("Speaker", {signal: 0, gnd: 1}) }, { assign: 0, obniz: obniz.wired("Speaker", {signal: 2, gnd: 3}) }, { assign: 0, obniz: obniz.wired("Speaker", {signal: 4, gnd: 7}) }, ) ioHook.on("keydown", event => { if (!keymap[event.keycode]) return if (speakers.some(speaker => speaker.assign === event.keycode)) return for (const speaker of speakers) { if (speaker.assign) continue speaker.assign = event.keycode speaker.obniz.play(keymap[event.keycode]) return } }) ioHook.on("keyup", event => { if (!keymap[event.keycode]) return for (const speaker of speakers) { if (speaker.assign !== event.keycode) continue speaker.assign = 0 speaker.obniz.stop() return } }) ioHook.start() }キー割り当て
こんな感じで音程を割り当てています。
演奏してる様子
ひとまずドレミファソラシド。
obnizと圧電スピーカーでキーボードをキーボード(音楽)化した。 pic.twitter.com/zwrHBUTPmE
— 田中みそ@LINE API Expertになりました! (@miso_develop) December 4, 2019スピーカー3つ繋いでるので3和音まで出せます。
(直差しじゃなければ12和音まで出せます)スピーカー3つ繋げてるので3和音まで可。
— 田中みそ@LINE API Expertになりました! (@miso_develop) December 4, 2019
(3つめのスピーカー音悪し) pic.twitter.com/YmWEjWEhkQ最後に一曲演奏してみます。
なんの曲かわかったらまじ神です。ちなみに所々音出てないのは回路やパーツやプログラムが悪いわけではなく、多分指を離すのが遅くて3和音鳴ってる状態のまま次のキー押しちゃってるのが問題です。
つまり演奏が下手なだけです。一曲弾いてみた。
— 田中みそ@LINE API Expertになりました! (@miso_develop) December 4, 2019
なんの曲か分かったらまじで神。
(所々音出てなかったりミスってたり…) pic.twitter.com/2KwPZphHS8おわりに
ここまで書いてそもそもこれはIoTなのか疑問になりましたが、obnizはネットにつながってないと動かないのでこれはきっとIoT。
- 投稿日:2019-12-04T23:04:04+09:00
NestJS でダミーの Service を注入し、外部依存のないテストを実行する
この記事は NestJS アドベントカレンダー 4 日目の記事です。
はじめに
先日は Module と DI について説明しましたが、本日はもう一歩進んだ DI を活用したテストを実施してみます。
なお、サンプルでは MySQL に接続したり Docker を使用したりしていますが、怖がらないでください。
この記事では MySQL や Docker に依存せずにテストできるようにするテクニックを説明します。サンプルコードのリポジトリは以下になります。
なお、環境は執筆時点での Node.js の LTS である v12.13.1 を前提とします。
サンプルアプリの雛形を作る
今回のサンプルとなるアプリケーションの雛形を cli を用いて作ってゆきます。
$ nest new day4-inject-dummy-service $ nest g module items $ nest g controller items $ nest g service itemsItemsController には以下のように Post と Get を実装していきます。
items/items.controller.ts@Controller('items') export class ItemsController { constructor(private readonly itemsService: ItemsService) {} @Post() async createItem(@Body() { title, body, deletePassword }: CreateItemDTO) { const item = await this.itemsService.createItem( title, body, deletePassword, ); return item; } @Get() async getItems() { const items = await this.itemsService.getItems(); return items; } }ItemsService も雛形を作成します。
items/items.service.ts@Injectable() export class ItemsService { async createItem(title: string, body: string, deletePassword: string) { return; } async getItems() { return []; } }MySQL にデータを書き込む箇所を実装する
今回は Service の外部依存先として、 MySQL を例にあげます。
MySQL に接続するため、以下のライブラリをインストールします。$ yarn add typeorm mysql
なお、今回は TypeORM の複雑な機能は極力使用せずにサンプルを記述します。
TypeORM についての説明や NestJS との組み合わせ方については別の記事で説明します。
また、本来は constructor で非同期の初期化を行うべきではないのですが、回避策は複雑なので、こちらも別途説明します。items/items.service.ts@Injectable() export class ItemsService { connection: Connection; constructor() { createConnection({ type: 'mysql', host: '0.0.0.0', port: 3306, username: 'root', database: 'test', }) .then(connection => { this.connection = connection; }) .catch(e => { throw e; }); } // connection が確立していないタイミングがあるため待ち受ける private async waitToConnect() { if (this.connection) { return; } await new Promise(resolve => setTimeout(resolve, 1000)); await this.waitToConnect(); } async createItem(title: string, body: string, deletePassword: string) { if (!this.connection) { await this.waitToConnect(); } await this.connection.query( `INSERT INTO items (title, body, deletePassword) VALUE (?, ?, ?)`, [title, body, deletePassword], ); } async getItems() { if (!this.connection) { await this.waitToConnect(); } const rawItems = await this.connection.query('SELECT * FROM items'); const items = rawItems.map(rawItem => { const item = { ...rawItem }; delete item.deletePassword; return item; }); return items; } }また、 MySQL を Docker で立ち上げます。
$ docker-compose up
Docker ではない MySQL で実行する場合、 MySQL に
test
データベースを作り、create-table.sql
を流してください。この状態でアプリケーションを起動してみましょう。MySQL が起動していれば、無事起動するはずです。
$ yarn start:dev
続いて curl でアプリケーションの動作確認をしてみます。
$ curl -XPOST -H 'Content-Type:Application/json' -d '{"title": "hoge", "body": "fuga", "deletePassword": "piyo"}' localhost:3000/items$ curl locaohost:3000/items [{"title":"hoge","body":"fuga"}]無事保存できるアプリケーションができました。
MySQL がない状態でもテストできるようにする
アプリケーションができたので、Mock を使ってテストを記述します。
前回までのサンプルでは特に DI を意識する必要がなかったため
new ItemsService()
としてテストを記述していましたが、
今回は DI に関連するため、 cli で自動生成される雛形にも用いられているTest
モジュールを使用します。describe('ItemsController', () => { let itemsController: ItemsController; let itemsService: ItemsService; beforeEach(async () => { const testingModule: TestingModule = await Test.createTestingModule({ imports: [ItemsModule], }).compile(); itemsService = testingModule.get<ItemsService>(ItemsService); itemsController = new ItemsController(itemsService); }); describe('/items', () => { it('should return items', async () => { expect(await itemsController.getItems()).toHaveLength(1); }); }); });さて、この状態でテストを実行するとどうなるでしょうか。
MySQL を起動している場合はそのままテストが通りますが、 MySQL を停止すると以下のようにテストが落ちてしまいます。$ jest PASS src/app.controller.spec.ts FAIL src/items/items.controller.spec.ts ● ItemsController › /items › should return items connect ECONNREFUSED 0.0.0.0:3306 -------------------- at Protocol.Object.<anonymous>.Protocol._enqueue (../node_modules/mysql/lib/protocol/Protocol.js:144:48) at Protocol.handshake (../node_modules/mysql/lib/protocol/Protocol.js:51:23) at PoolConnection.connect (../node_modules/mysql/lib/Connection.js:119:18) at Pool.Object.<anonymous>.Pool.getConnection (../node_modules/mysql/lib/Pool.js:48:16) at driver/mysql/MysqlDriver.ts:869:18 at MysqlDriver.Object.<anonymous>.MysqlDriver.createPool (driver/mysql/MysqlDriver.ts:866:16) at MysqlDriver.<anonymous> (driver/mysql/MysqlDriver.ts:337:36) at step (../node_modules/tslib/tslib.js:136:27) at Object.next (../node_modules/tslib/tslib.js:117:57) Test Suites: 1 failed, 1 passed, 2 total Tests: 1 failed, 1 passed, 2 total Snapshots: 0 total Time: 1.204s, estimated 3sItemsService を Mock していますが、 ItemsService の初期化自体はされており、初期化処理の中で MySQL への接続しようとしているのが原因です。
このような、 外部へ依存する Provider の初期化 をテストから除外するために、 ItemsService を上書きした状態でtestingModule
を生成する機能が NestJS には備わっています。以下のように
DummyItemsService
class を定義し、overrideProvider
を使って上書きします。class DummyItemsService { async createItem(title: string, body: string, deletePassword: string) { return; } async getItems() { const item = { id: 1, title: 'Dummy Title', body: 'Dummy Body', }; return [item]; } } describe('ItemsController', () => { let itemsController: ItemsController; let itemsService: ItemsService; beforeEach(async () => { const testingModule: TestingModule = await Test.createTestingModule({ imports: [ItemsModule], }) .overrideProvider(ItemsService) .useClass(DummyItemsService) .compile(); itemsService = testingModule.get<ItemsService>(ItemsService); itemsController = new ItemsController(itemsService); }); describe('/items', () => { it('should return items', async () => { expect(await itemsController.getItems()).toHaveLength(1); }); }); });
useClass()
の代わりにuseValue()
を使うことで、 class ではなく変数で上書きすることもできます。この状態でテストを実行すると、 MySQL が起動していなくても問題なく通過します。
yarn run v1.19.0 $ jest PASS src/items/items.controller.spec.ts PASS src/app.controller.spec.ts Test Suites: 2 passed, 2 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 2.406s Ran all test suites. ✨ Done in 2.94s.おわりに
この記事で NestJS の持つ強力な DI の機能をお伝えできたかと思います。
より詳細な内容は公式のドキュメントの E2E テストの項にあるので、合わせてご確認ください。
https://docs.nestjs.com/fundamentals/testing#end-to-end-testingまた、今回説明できなかった TypeORM との合わせ方や、非同期の初期化を必要とする Service の扱い方については、後日別の記事で説明します。
明日は @potato4d さんが ExceptionFilter についてお話する予定です。
- 投稿日:2019-12-04T21:35:29+09:00
Node.jsのHTTPリクエストヘッダの最大サイズでハマった話
現象
- Node.js(v12.3.1)で立てたWebサーバにアクセスすると、時折HTTPリクエストに失敗する
- Cookieを削除したり、ブラウザを再起動すると治ることもあるが、根本的な原因がわからない
サンプルコードconst http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World'); }); server.listen(8080);原因
Node.jsの最大HTTPリクエストヘッダサイズのデフォルト値である8kBを越えるHTTPリクエストヘッダサイズを送信していたことが原因だった
Node.jsは、2018/11にDoS攻撃の脆弱性対応として、デフォルトのHTTPリクエストヘッダの最大サイズを変更前の80kBから8kB(8192Bytes)に変更する修正が加えられた
デフォルトでは、HTTPリクエストヘッダのサイズが8kBを越えるとソケットが強制破棄されて「431 Request Header Fields Too Large」を返す
$ npm start > sample-nodejs-header-overflow@1.0.0 start /../../../sample-nodejs-header-overflow > node index.js // curlで8kB以上のHTTPリクエストを送信 ErrorCode: HPE_HEADER_OVERFLOW BytesParsed: 8559 // curlで8kB以上のHTTPリクエストを送信 ErrorCode: HPE_HEADER_OVERFLOW BytesParsed: 8559 // curlで8kB以上のHTTPリクエストを送信 ErrorCode: HPE_HEADER_OVERFLOW BytesParsed: 9085対策
- HTTPリクエストヘッダのサイズが超えた場合に起こるclientErrorイベントを補足して、ソケットが強制的に破棄されないようにエラーハンドリングを行う
const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World'); }); server.on('clientError', (err, socket) => { console.log('ErrorCode: ', err.code); console.log('BytesParsed: ', err.bytesParsed); socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); }); server.listen(8080);
- アプリケーション起動時に「--max-http-header-size」という起動オプションを設定して、Node.jsが受け取る最大のHTTPリクエストヘッダサイズを増やす
$ node --max-http-header-size=16384 index.jsおわりに
今回リクエストヘッダのサイズが8kBを越えた主な原因は、多数のCookieを使ってWebサーバにアクセスしていたことでした。
仕様上Cookieの数が多くなり、HTTPリクエストのサイズに不安がある場合は、エラーハンドリングを正しく実装して、起動オプションでNode.jsが受け取るHTTPリクエストサイズの最大値を上げておくと良いかと思います。参考
- 投稿日:2019-12-04T18:00:01+09:00
ソフトウェア初心者がtoio.jsで作ってみた 5つの作例紹介
これは「toio™(ロボットトイ | toio(トイオ)) Advent Calendar 2019」の8日目の記事になります。
はじめに
はじめまして。ヒラノユウヤです。
普段はハードウェアエンジニア(電気)として暮らしています。
この記事では、ソフトウェア初心者の私がtoio.jsを使って作ってみたtoio作品を紹介したいと思います。ソフトウェアスキル
- C言語
- 学校の授業では真面目に取り組んでいました
- 社会人になってからも、Arduinoを使いこなすくらいには使っていた感じ
以上。なんとも貧弱で泣けてきます。
なんですが、toio core cubeを使ったプログラミングがどうしてもやりたくて。
toio.jsの環境を友人に手伝って構築してもらったところからスタートしました。
始めてみると、サンプルコードもあるので、苦労はしながらも意外といろんなものができました。参考にしたもの
1にも2にも、公式情報が命でした。
用意されているtoio.jsの使い方はtoio.jsのページで。
buzzerの音階やtoio IDの情報など、toio自体に対しての情報は技術仕様のページで。
toio.jsのページ
-- https://toio.github.io/toio.js/globals.htmltoioコアキューブ技術仕様のページ
-- https://toio.github.io/toio-spec/あとはサンプルプログラムの読み解きと、ちょい変でのトライ&エラーを繰り返しました。
作例紹介
早速紹介始めます。
実際に作ってtwitterに上げたのは結構昔なので、記憶を辿りながら文章書いてみます。
ソースコードもまんま貼り付けるので、批判称賛なんでもコメントいただければ嬉しいです。1.モールス信号発生器
夜な夜なプログラミング遊び
— Yuya Hirano @ BREMENGames ゲムマ秋Q15 (@idiot_radio_hy) June 29, 2019
toioを使ってモールス信号発生器作った。
キーボードを押すとそのアルファベットに応じたモールス信号が鳴ります。
動画はSOS打ってみた。これでいつ倒れても救助呼べる(違)#toio pic.twitter.com/TNaUkblCqHパソコンのキーボード入力の取得と、toio.jsのplaySound()の組み合わせです。
キーボード入力の取得はtoio.jsのサンプルプログラム keyboard-control から拝借しました。
入力されたアルファベットをcase文で場合分けします。
対応するモールス信号の構造体を生成して、それをCubeのブザーから鳴らしています。モールス信号は法則性がないので、このようなcase文での力技しか方法が思いつきませんでした。
const keypress = require('keypress') const { NearestScanner } = require('@toio/scanner') const TONE = 64 const TONE_SILENT = 127 const DURATION_SHORT = 200 const DURATION_LONG = DURATION_SHORT * 3 var morse_short = [ { durationMs: DURATION_SHORT, noteName: TONE }, { durationMs: DURATION_SHORT, noteName: TONE_SILENT }, ] var morse_long = [ { durationMs: DURATION_LONG, noteName: TONE }, { durationMs: DURATION_SHORT, noteName: TONE_SILENT }, ] var morse async function main() { // start a scanner to find nearest cube const cube = await new NearestScanner().start() // connect to the cube await cube.connect() keypress(process.stdin) process.stdin.on('keypress', (ch, key) => { if ((key && key.ctrl && key.name === 'c')) { process.exit() } switch (key.name) { case 'a': morse = morse_short.concat(morse_long) cube.playSound(morse ,1) break case 'b': morse = morse_long.concat(morse_short).concat(morse_short).concat(morse_short) cube.playSound(morse ,1) break case 'c': morse = morse_long.concat(morse_short).concat(morse_long).concat(morse_short) cube.playSound(morse ,1) break case 'd': morse = morse_long.concat(morse_short).concat(morse_short) cube.playSound(morse ,1) break case 'e': morse = morse_short cube.playSound(morse ,1) break case 'f': morse = morse_short.concat(morse_short).concat(morse_long).concat(morse_short) cube.playSound(morse ,1) break case 'g': morse = morse_long.concat(morse_long).concat(morse_short) cube.playSound(morse ,1) break case 'h': morse = morse_short.concat(morse_short).concat(morse_short).concat(morse_short) cube.playSound(morse ,1) break case 'i': morse = morse_short.concat(morse_short) cube.playSound(morse ,1) break case 'j': morse = morse_short.concat(morse_long).concat(morse_long).concat(morse_long) cube.playSound(morse ,1) break case 'k': morse = morse_long.concat(morse_short).concat(morse_long) cube.playSound(morse ,1) break case 'l': morse = morse_short.concat(morse_long).concat(morse_short).concat(morse_short) cube.playSound(morse ,1) break case 'm': morse = morse_long.concat(morse_long) cube.playSound(morse ,1) break case 'n': morse = morse_long.concat(morse_short) cube.playSound(morse ,1) break case 'o': morse = morse_long.concat(morse_long).concat(morse_long) cube.playSound(morse ,1) break case 'p': morse = morse_short.concat(morse_long).concat(morse_long).concat(morse_short) cube.playSound(morse ,1) break case 'q': morse = morse_long.concat(morse_long).concat(morse_short).concat(morse_long) cube.playSound(morse ,1) break case 'r': morse = morse_short.concat(morse_long).concat(morse_short) cube.playSound(morse ,1) break case 's': morse = morse_short.concat(morse_short).concat(morse_short) cube.playSound(morse ,1) break case 't': morse = morse_long cube.playSound(morse ,1) break case 'u': morse = morse_short.concat(morse_short).concat(morse_long) cube.playSound(morse ,1) break case 'v': morse = morse_short.concat(morse_short).concat(morse_short).concat(morse_long) cube.playSound(morse ,1) break case 'w': morse = morse_short.concat(morse_long).concat(morse_long) cube.playSound(morse ,1) break case 'x': morse = morse_long.concat(morse_short).concat(morse_short).concat(morse_long) cube.playSound(morse ,1) break case 'y': morse = morse_long.concat(morse_short).concat(morse_long).concat(morse_long) cube.playSound(morse ,1) break case 'z': morse = morse_long.concat(morse_long).concat(morse_short).concat(morse_short) cube.playSound(morse ,1) break } }) process.stdin.setRawMode(true) process.stdin.resume() } main()2.電子ピアノ
夜な夜なプログラミング遊び 2
— Yuya Hirano @ BREMENGames ゲムマ秋Q15 (@idiot_radio_hy) June 30, 2019
toioを電子ピアノ化しました。
マットの座標に伴い、Cメジャースケールで音を鳴らすピアノ。
動画ではドレミの歌ときらきら星しか演奏してませんが、行が変わればオクターブ高くなるので、小室哲哉見たいな演奏も可能。笑#toio pic.twitter.com/qPjnSU5Y0b1.でBuzzerが鳴らせたので、今度は読み取りセンサと合わせたものが作りたいと言うことで、作ったものです。
読み取りセンサでトイオ・コレクションのマット座標を読み取って、対応する音をブザーから鳴らしています。
読み取りセンサの値はそのまま使うのではなく、トイオ・コレクションのマットの格子単位の単位で検出するように丸めています。
ここの丸めかた、実物合わせで採寸しながらやりました。Cubeがマットに触れている間だけ音が鳴るように、
Cubeがマットに載った時に動く関数 cube.on('id:position-id' で音を鳴らして
Cubeがマットから離れた時に動く関数 cube.on('id:position-id-missed' で音を消す処理を入れています。実はここで複数Cube接続できるようにコードを修正しています。
起動時にキーボード入力で入力した数自分のCubeを接続できるようにしています。
私の環境では最大6台までのCubeの接続ができました。const keypress = require('keypress') const { NearScanner } = require('@toio/scanner') var midi_note = new Array() var data_norm = new Array() data_norm[0] = {x:0,y:0} const DURATION = 3000 var MIDI_SCALE_C = [0,0,2,4,5,7,9,11,12,12,12] const X_INI_TOICOLE = 555.5 const X_END_TOICOLE = 946.95 const Y_INI_TOICOLE = 53 const Y_END_TOICOLE = 44.95 const UNIT_TOICOLE = 43.2 var cube_number = 2 function cube_control(cube){ var lastData = {x:0, y:0} var flag = 0 cube.on('id:position-id', data1 => { var tmp = {x: Math.floor((data1.x - X_INI_TOICOLE) / UNIT_TOICOLE) + 1, y: Math.floor((data1.y - Y_INI_TOICOLE) / UNIT_TOICOLE) + 1} if (tmp.x != lastData.x) flag = 0 if (tmp.y != lastData.y) flag = 0 midi_note = MIDI_SCALE_C[tmp.x] + (tmp.y -1)* 12 if (flag==0){ cube.playSound([{durationMs: DURATION, noteName: midi_note}] ,1) flag = 1 } lastData = tmp console.log('[X_STEP]', tmp.x) console.log('[Y_STEP]', tmp.y) console.log('MIDI',midi_note) } ) cube.on('id:position-id-missed', () => { flag = 0 cube.stopSound() console.log('[POS ID MISSED]') } ) } async function cube_connect(cube_number){ // start a scanner to find the nearest cube const cubes = await new NearScanner(cube_number).start() // connect to the cube for(var i = 0; i < cube_number; i++) {await cubes[i].connect()} return cubes } async function main() { console.log('USE Rhythm and Go Mat') console.log('Press connect cube number') keypress(process.stdin) process.stdin.on('keypress', async (ch, key) => { // ctrl+c or q -> exit process if(key){ if ((key && key.ctrl && key.name === 'c') || (key && key.name === 'q')) { process.exit() } }else{ console.log('[Ch]',ch) cube_number = ch const cubes = await cube_connect(ch) for(var i = 0; i < cube_number; i++) {cube_control(cubes[i])} } } ) process.stdin.setRawMode(true) process.stdin.resume() } main()3.宝探しゲーム
夜な夜なプログラミング遊び3
— Yuya Hirano @ BREMENGames ゲムマ秋Q15 (@idiot_radio_hy) July 2, 2019
toio.jsで宝探しゲーム作った。
宝のある目標座標から遠ざかるとLEDが強くなる。
x方向は緑、y方向は青、回転方向は赤。
x.y.回転の全てを目標座標と一致したらRGBLEDが全て消え、ブザーの音がなる。
プログラミング初心者だけど少しずつ出来ることが増えてきた!#toio pic.twitter.com/WfCeMspwy5今度はLEDの点灯と組み合わせを試してみた作品です。
ランダムに生成されるゴール位置をLEDの色を見ながら手探りで探し当てるといったゲームを作りました。マット上にCubeを置くと、座標(X,Y)と姿勢(Θ)が取得できます。
ゴールの場所(X,Y,Θ)から遠ざかるほどLED色が強くなり、Target場所に一致すると消える という仕様。
つまり、LEDの光が消える場所をさがす というゲームです。X方向は赤、Y方向は緑、Θ方向は青
といったように各軸で別の色のLEDが反応するので、色味を見ながらどっちの方向に動かすかを考えます。ゴールの位置にみごCubeを持っていくことができたら勝利判定し、勝利のファンファーレを鳴らすようにしています。
melody_win, melody_lose のやたら長い構造体はこのファンファーレの音データです。const keypress = require('keypress') const { NearScanner } = require('@toio/scanner') var midi_note = new Array() var data_norm = new Array() var ledData = new Array() var target = new Array() var diff = new Array() data_norm[0] = {x:0,y:0} const DURATION = 0 ledData = {durationMs:DURATION, red:255, green:255, blue:255} const X_INI_TOICOLE = 555.5 const Y_INI_TOICOLE = 53 const UNIT_TOICOLE = 43.2 const X_BEGIN_TOICOLE = 45 const X_END_TOICOLE = 455 const Y_BEGIN_TOICOLE = 45 const Y_END_TOICOLE = 455 const ANGLE_FULLSCALE = 360 var cube_number = 2 target = {x: Math.round(Math.random()*(X_END_TOICOLE - X_BEGIN_TOICOLE))+X_BEGIN_TOICOLE, y: Math.round(Math.random()*(X_END_TOICOLE - X_BEGIN_TOICOLE))+X_BEGIN_TOICOLE, angle: Math.round(Math.random()*ANGLE_FULLSCALE)} diff = {x:0,y:0,angle:0} var melody_win = [ { durationMs: 400, noteName: 127 }, { durationMs: 400, noteName: 60 }, { durationMs: 100, noteName: 72 }, { durationMs: 100, noteName: 127 }, { durationMs: 100, noteName: 67 }, { durationMs: 100, noteName: 127 }, { durationMs: 100, noteName: 72 }, { durationMs: 100, noteName: 127 }, { durationMs: 600, noteName: 75 }, { durationMs: 100, noteName: 77 }, { durationMs: 100, noteName: 127 }, { durationMs: 100, noteName: 77 }, { durationMs: 100, noteName: 127 }, { durationMs: 100, noteName: 77 }, { durationMs: 100, noteName: 127 }, { durationMs: 1600, noteName: 79 }, ]; var melody_lose = [ { durationMs: 5000, noteName: 127 }, { durationMs: 3000, noteName: 127 }, { durationMs: 150, noteName: 71 }, { durationMs: 150, noteName: 77 }, { durationMs: 150, noteName: 127 }, { durationMs: 150, noteName: 77 }, { durationMs: 200, noteName: 77 }, { durationMs: 200, noteName: 76 }, { durationMs: 200, noteName: 74 }, { durationMs: 200, noteName: 72 }, ]; var flag_gloval = 0 var winnerCubeId =0 function cube_control(cube){ var lastData = {x:0, y:0, angle:0} var lastData2 = {x:0, y:0, angle:0} var flag = 0 var flag_2 = 0 cube.on('id:position-id', data1 => { var tmp = {x: Math.floor((data1.x - X_INI_TOICOLE) / UNIT_TOICOLE) + 1, y: Math.floor((data1.y - Y_INI_TOICOLE) / UNIT_TOICOLE) + 1, angle: data1.angle} //angle calc diff.angle = Math.abs(target.angle - data1.angle) if(diff.angle > 180) diff.angle = 360 - diff.angle //xy calc diff.x = Math.abs(target.x - data1.x) diff.y = Math.abs(target.y - data1.y) //Thinning if (Math.abs(data1.x - lastData2.x) > 3) flag_2 = 0 if (Math.abs(data1.y - lastData2.y) > 3) flag_2 = 0 if (Math.abs(data1.angle - lastData2.angle) > 3) flag_2 = 0 if (flag_gloval==1 && flag ==0){ if(cube.id == winnerCubeId) cube.playSound(melody_win,1) else cube.playSound(melody_lose,1) console.log('[WIN!]') flag = 1 } if (flag_2==0){ ledData.red = Math.floor(diff.angle / 360 *20)*25 ledData.green = Math.floor(diff.x / 410 *20)*25 ledData.blue = Math.floor(diff.y / 410 *20)*25 //winner judge if ((ledData.red + ledData.green + ledData.blue) == 0) { winnerCubeId = cube.id flag_gloval = 1 } cube.turnOnLight(ledData) flag_2 = 1 //position store lastData2 = data1 } console.log('[Winner,cubeID]',winnerCubeId,cube.id) console.log(target) console.log(ledData) console.log(diff) console.log(data1) console.log(lastData2) } ) cube.on('id:position-id-missed', () => { flag = 0 flag_2 = 0 flag_gloval = 0 cube.stopSound() //cube.turnOffLight() console.log('[POS ID MISSED]') } ) } async function cube_connect(cube_number){ // start a scanner to find the nearest cube const cubes = await new NearScanner(cube_number).start() // connect to the cube for(var i = 0; i < cube_number; i++) {await cubes[i].connect()} return cubes } async function main() { console.log('USE Craft fighter Mat') console.log('Press connect cube number') keypress(process.stdin) process.stdin.on('keypress', async (ch, key) => { // ctrl+c or q -> exit process if(key){ if ((key && key.ctrl && key.name === 'c') || (key && key.name === 'q')) { process.exit() } }else{ console.log('[Ch]',ch) cube_number = ch //connect cube const cubes = await cube_connect(ch) //control cube for(var i = 0; i < cube_number; i++) {cube_control(cubes[i])} } } ) process.stdin.setRawMode(true) process.stdin.resume() } main()4.和音プレイヤー
夜な夜なプログラミング遊び 4
— Yuya Hirano @ BREMENGames ゲムマ秋Q15 (@idiot_radio_hy) July 4, 2019
toio.jsでブザーのオーケストラ作った。
1つのcubeがマットに触れると、ほかの3つのcubeが和音を奏でます。
縦マスは音階、横マスはコードの種類。マイナーコードもお手の物。
新たな楽器を生み出しているようで楽しい!#toio pic.twitter.com/tdN5KpKvww複数Cubeの連携制御に挑戦したく、作った作品です。
1つのCubeがマットに触れると、格子ごとに他の3つのCubeが異なるコードを演奏します。和音なので、3台のCubeでタイミングを合わせた音再生をするのをどうしたらいいか? といろいろ考えましたが、
今回は
Cube1のマットON判定の関数の中でCube2/3/4のBuzzer音再生を行う
ことでこれを実現できました。const { NearScanner } = require('@toio/scanner') var midi_note = new Array() var data_norm = new Array() data_norm[0] = {x:0,y:0} const DURATION = 3000 var MIDI_SCALE_C = [0,0,2,4,5,7,9,11,12,12,12] var scaleList = ["C","C","D","E","F","G","A","B","C","D","D","D"] var codeList = ["M","m","7","sus4","M7","m7-5","aug","add9","6"] const X_INI_TOICOLE = 555.5 const X_END_TOICOLE = 946.95 const Y_INI_TOICOLE = 53 const Y_END_TOICOLE = 44.95 const UNIT_TOICOLE = 43.2 var cube_number = 4 var scale = 0 var type = 0 var midi_note = [ {uno:60, dos:64, tre:67}, //C major {uno:60, dos:63, tre:67}, //m {uno:58, dos:64, tre:67}, //7 {uno:60, dos:65, tre:67}, //sus4 {uno:59, dos:64, tre:67}, //M7 {uno:60, dos:63, tre:66}, //m7-5 {uno:60, dos:64, tre:68}, //aug {uno:60, dos:62, tre:67}, //add9 {uno:60, dos:64, tre:69}, //6 ] function codeController(cubes){ var lastData = {x:0, y:0} var flag = 0 cubes[0].on('id:position-id', data1 => { var tmp = {x: Math.floor((data1.x - X_INI_TOICOLE) / UNIT_TOICOLE) + 1, y: Math.floor((data1.y - Y_INI_TOICOLE) / UNIT_TOICOLE) + 1} if (tmp.x != lastData.x) flag = 0 if (tmp.y != lastData.y) flag = 0 if (flag==0){ scale = tmp.y type = tmp.x - 1 cubes[1].playSound([{durationMs: DURATION, noteName: midi_note[type].uno + MIDI_SCALE_C[scale]}] ,1) cubes[2].playSound([{durationMs: DURATION, noteName: midi_note[type].dos + MIDI_SCALE_C[scale]}] ,1) cubes[3].playSound([{durationMs: DURATION, noteName: midi_note[type].tre + MIDI_SCALE_C[scale]}] ,1) cubes[1].turnOnLight({durationMs:DURATION, red:0, green:255, blue:255}) cubes[2].turnOnLight({durationMs:DURATION, red:255, green:0, blue:255}) cubes[3].turnOnLight({durationMs:DURATION, red:255, green:255, blue:0}) flag = 1 console.log('[CODE]', scaleList[scale],codeList[type]) } lastData = tmp } ) cubes[0].on('id:standard-id', data2 => console.log('[STD ID]', data2)) cubes[0].on('id:position-id-missed', () => { flag = 0 cubes[1].stopSound() cubes[2].stopSound() cubes[3].stopSound() cubes[1].turnOffLight() cubes[2].turnOffLight() cubes[3].turnOffLight() } ) cubes[0].on('id:standard-id-missed', () => console.log('[STD ID MISSED]')) } function init(cubes){ cubes[0].turnOnLight({durationMs:DURATION, red:100, green:100, blue:100}) } async function main() { console.log('4cubes') console.log('USE Rhythm and Go Mat') // start a scanner to find the nearest cube const cubes = await new NearScanner(cube_number).start() // connect to the cube for(var i = 0; i < cube_number; i++) {await cubes[i].connect()} init(cubes) codeController(cubes) } main()5.マスゲーム
夜な夜なプログラミング遊び 5
— Yuya Hirano @ BREMENGames ゲムマ秋Q15 (@idiot_radio_hy) July 4, 2019
toio.jsで複数台同時位置制御。マスゲームみたいなものが出来ました。
単純な動きでも複数台協調すると面白みが生まれるのは不思議です。
OK GOのPVぽい。まかり間違ってオファーこないかしら。笑 pic.twitter.com/imRcpDHgmaCubeはやはり動かなきゃ!ということで、モーター制御が使いたくて作った作品です。
モーターを動かすところはtoio.jsのサンプルプログラム chase を参考にしています。動きとしては極めて単純で、一定時間ごとに異なる目的地へCubeを制御しているだけ。
ただ、この「一定時間ことに」が曲者でした。
toio.jsはイベントドリブンなサンプルコードになっているので、「一定時間ごとに」実行するためのコードの書き方がわかりませんでした。ここは友人に頼りまして、最強の武器
setinterval()
を教えてもらいました。これを使うことで「一定時間ごと」の処理が記述できました。単純な動きでも、4つ組み合わさると、面白味が生まれますね。
const { NearScanner } = require('@toio/scanner') var midi_note = new Array() var data_norm = new Array() var ledData = new Array() var target = new Array() var diff = new Array() var cubePos = new Array() const X_INI_TOICOLE = 555.5 const Y_INI_TOICOLE = 53 const UNIT_TOICOLE = 43.2 const X_BEGIN_TOICOLE = 45 const X_END_TOICOLE = 455 const Y_BEGIN_TOICOLE = 45 const Y_END_TOICOLE = 455 const ANGLE_FULLSCALE = 360 const CUBE_WIDTH = 32 target[0] = {x:145,y:145,angle:90} target[1] = {x:355,y:145,angle:0} target[2] = {x:355,y:355,angle:270} target[3] = {x:145,y:355,angle:180} cubePos[0] = {x:0,y:0,angle:0} cubePos[1] = {x:0,y:0,angle:0} cubePos[2] = {x:0,y:0,angle:0} cubePos[3] = {x:0,y:0,angle:0} data_norm[0] = {x:0,y:0} const DURATION = 0 var cube_number = 4 var flag_gloval = 0 function MoveToTarget(target,mine){ const diffX = target.x - mine.x const diffY = target.y - mine.y const distance = Math.sqrt(diffX * diffX + diffY * diffY) //calc angle var relAngle = (Math.atan2(diffY, diffX) * 180) / Math.PI - mine.angle relAngle = relAngle % 360 if (relAngle < -180) { relAngle += 360 } else if (relAngle > 180) { relAngle -= 360 } const ratio = 1 - Math.abs(relAngle) / 90 let speed = 60 * distance /210 if (distance < 10) { return [0, 0] // stop } if (relAngle > 0) { return [speed, speed * ratio] } else { return [speed * ratio, speed] } } function cube_control(cube,cubePosition){ var lastData = {x:0, y:0, angle:0} var lastData2 = {x:0, y:0, angle:0} var flag = 0 var flag_2 = 0 cube.on('id:position-id', data1 => { cubePosition.x = data1.x cubePosition.y = data1.y cubePosition.angle = data1.angle } ) } function setTarget(){ var tmp = target[0] target[0] = target[1] target[1] = target[2] target[2] = target[3] target[3] = tmp } async function main() { console.log('4cubes') console.log('USE Craft fighter Mat') // start a scanner to find the nearest cube const cubes = await new NearScanner(cube_number).start() // connect to the cube for(var i = 0; i < cube_number; i++) {await cubes[i].connect()} for(var i = 0; i < cube_number; i++) {cube_control(cubes[i],cubePos[i])} // loop setInterval(() => { for(var i = 0; i < cube_number; i++) cubes[i].move(...MoveToTarget(target[i],cubePos[i]), 100) }, 50) setInterval(() => { setTarget() }, 3000) } main()さいごに
友人達のサポートも多々ありましたが、初心者でもやればできるものですね。
javascriptはC言語と違って、イベントドリブンでの処理を書くのがとても簡単に出来ているように感じました。
C言語だと、割り込み処理で書かなきゃいけないところが、関数宣言しとけば勝手に実行される みたいな。
処理を『置いておく』感覚で簡単にプログラミングできるのが良かったです。またいろいろと面白い動きを作っていきたいと思います。
- 投稿日:2019-12-04T17:36:51+09:00
Aurora Serverless DB を作って Node.js(TS) から使う
概要
Aurora Serverless DB を作成して、
Node.js (TypeScript) からアクセスしてみます。実行時の環境 2019/12/04
- MacOS 10.14.4
- node v10.15.0
- npm 6.6.0
- ts-node v8.5.4
- aws-sdk 2.584.0
DB の作成
Data API 公式ドキュメントを見ると、
現在、Data API が有効なリージョンは限られているらしいので注意
東京リージョンでつくる。
- DB 作成方法
項目 値 テータベース作成方法 標準作成
- エンジンのオプション
項目 値 エンジンのタイプ Amazon Aurora エディション MySQL 互換 バージョン 現行最新: Aurora (MySQL)-5.6.10a データベースロケーション リージョン別
- データベースの機能
項目 値 データベースの機能 サーバーレス
設定
- マスターパスワード はあとでテストに出るのでノートにとること
キャパシティーの設定
ACU
=Aurora キャパシティーユニット
- 使用する ACU x 時間に応じて課金が発生する。デフォルト最大値 128 とかいってて怖いから 8 に下げて様子見る。
- コールドスタート
- 使ってない時間帯は勝手に止まってくれる
- 使い始めは 1 分かけてゆるく起動するらしい
- 開発環境とか社内向けサービスなのでゆるくていい
項目 値 最小 ACU 1 最大 ACU 8 追加設定 アイドルの場合、コンピューティングを一時停止: 15 分
- 接続
Data API
を有効にする- そのほかはてきとう
項目 値 ウェブサービスデータ API Data API
- 追加設定
- 基本的にデフォルトのままにした。
最初のデータベース名
…DB 名とは違うのか?
- MySQL でいう
データベース
はスキーマ
と同じと考えていいらしい- ややこしいわ
- 複数のサービスで DB を利用する予定なので、はじめにのせるサービス名にした
DB ができた
作成中
ステータスで表示された- しばらくまつと
利用可能
になったQuery Editor を使って DB にユーザーを作る
Query Editor への接続
- RDS メニューから
Query Editor
へアクセス
- 作成した DB を選択
- user: admin
- password: さっきメモっといたマスターパスワード
- メモっとかなかったおバカさん(俺)は、DB の設定変更から再設定
- データベースに接続
- コンソールが開き、デフォルトのクエリが実行できたら OK
- せっかくなので、
SHOW DATABASES;
してみる
- DB 作成時に
最初のデータベース名
に入力していたデータベース(スキーマ)が表示されるはず開発用ユーザーを作り、データベースへのアクセス権を与える
in-QueryEditorCREATE USER 'devuser'@'%' IDENTIFIED BY 'YOUR_PASSWORD'; GRANT ALL ON (最初のデータベース名).* TO devuser;成功を確認したら、作ったユーザーでアクセスしてみる
- 「データベースを変更する」
- user:
devuser
- password:
YOUR_PASSWORD
- データベースまたはスキーマ:
(最初のデータベース名)
- 接続してクエリが実行できたら OK
Node.js から DB に接続する
これがやりたかった
Data API 公式ドキュメント から必要な部分を実行していく
Data API にアクセスするためのシークレットを作る
- Secret Managerから、MySQL ユーザーに対応する Secret を発行する必要がある。
- しかし、なんと Query Editor からアクセスした時点で Secret が勝手に作られている。便利。
アクセスするサンプルコードを書いて実行してみる (TypeScript)
src/aurora-test.tsimport { RDSDataService } from "aws-sdk"; import { ExecuteStatementRequest } from "aws-sdk/clients/rdsdataservice"; (function testQuery() { const rds = new RDSDataService({ region: "ap-northeast-1", accessKeyId: "***", secretAccessKey: "***" }); const params: ExecuteStatementRequest = { resourceArn: "***", // RDS > データベース > 設定 から参照 secretArn: "***", // SecretManager > 追加したユーザーのSecret > シークレットのARN database: "(最初のデータベース名)", sql: "select * from information_schema.tables", includeResultMetadata: true }; rds.executeStatement(params, (err, data) => { if (err) { console.error(err, err.stack); } else { console.log(`Fetch ${data.records!.length} rows!`); console.log(data.columnMetadata!.map(col => col.name).join(",")); for (const record of data.records!) { console.log(record.map(col => Object.values(col)[0]).join(",")); } } }); })();
ts-node
で実行ts-node src/aurora-test.ts > Fetch 69 rows! > TABLE_CATALOG,TABLE_SCHEMA,TABLE_NAME,TABLE_TYPE,ENGINE,VERSION,ROW_FORMAT,TABLE_ROWS,AVG_ROW_LENGTH,DATA_LENGTH,MAX_DATA_LENGTH,INDEX_LENGTH,DATA_FREE,AUTO_INCREMENT,CREATE_TIME,UPDATE_TIME,CHECK_TIME,TABLE_COLLATION,CHECKSUM,CREATE_OPTIONS,TABLE_COMMENT > def,information_schema,CHARACTER_SETS,SYSTEM VIEW,MEMORY,10,Fixed,true,384,0,16434816,0,0,true,2019-12-04 07:35:09,true,true,utf8_general_ci,true,max_rows=43690, > def,information_schema,COLLATIONS,SYSTEM VIEW,MEMORY,10,Fixed,true,231,0,16704765,0,0,true,2019-12-04 07:35:09,true,true,utf8_general_ci,true,max_rows=72628, > ...UTF-8 を指定してUnicodeを扱えるようにする
デフォルトの character set が
latin
とかいうやつで、
日本語が全部???
になって困ったので設定を変える。
別記事に切り出したSSH Tunnel (ec2踏み台) を使って直接接続する
Aurora Serverless はpublic ipを持てない。
ふつーに自由にクエリ書きたいときに困るよねってことで、
@hhrrwwttrr さんにおねがいして、踏み台EC2を作ってもらった。
踏み台を準備してもらうと、Sequel Pro などのクライアントからも直接SSH経由で接続できて便利。作る手順とかは @hhrrwwttrr さんがわかりやすく書いてくれるって言ってた。
できあがり
認証情報の扱いにはきをつけてつかおうね
- 投稿日:2019-12-04T12:22:13+09:00
EventEmitterですべてのイベントを取得する(ワイルドカード)
Node.jsのEventEmitterは便利なんですがすべてのイベントを取得できません。
何で実装していないのかよくわかりません。EventEmitter2というEventEmitterを便利にしたものがありこれを使えば良いのですが、更新が止まっています...
頑張ればできるんじゃねと思って書いたら数分でできたのメモ的な意味を込めて記事にしてます。
コード
index.js// いつものEventEmitter const EventEmitter = require("events"); // いつものEventEmitterを拡張 class ExtendEventEmitter extends EventEmitter { // emitされた内容を"*"に再emit emit(name, ...args) { return super.emit("*", name, ...args); } } // 拡張したEventEmitter const event = new ExtendEventEmitter(); // ワイルドカードでイベントを受ける event.on("*", (name, ...callback) => { console.log(`name: ${name} |`, ...callback); }); /* emit */ event.emit("ready", "ready..."); event.emit("number", 1, 2, 3, 4); event.emit("array", ["a", "b"]); event.emit("object", { "abc": 123, "def": 456 });結果
name: ready | ready... name: number | 1 2 3 4 name: array | [ 'a', 'b' ] name: object | { abc: 123, def: 456 }
- 投稿日:2019-12-04T11:48:01+09:00
React アプリケーションのボイラープレート CLI を作って使っている話
この記事は ミクシィグループ Advent Calendar 2019 の5日目の記事です。
React で CLI というと create-react-app が有名です。
格好良いベースを作ってくれるのですが個人的には依存 package が多いので、自分用の CLI を作ってそちらを使っています。@yami-beta/create-ts-app
TypeScript を使ったアプリケーションのベースを作る対話型のインターフェースを持った CLI ツールです。
https://www.npmjs.com/package/@yami-beta/create-ts-app
意外と色々な package を用意する必要がある ESLint + Prettier の設定を含めていたり、author や LICENSE を設定できます。
(あくまで個人用なので自分の好みによせたボイラープレートになっています)現在は React のシンプルなボイラープレートしかありませんが
- React, React Router, Redux 等が含まれた Single Page Application
- express によるサーバアプリケーション
のボイラープレートを追加していく予定です。
仕組み
この CLI ですが SAO というライブラリを使って実装しています。
(create-nuxt-app も SAO を利用していたりします)以下のようなコードを書くことで対話型のインターフェースを用意したり、テンプレートからファイルをコピーやリネームといったことが出来ます。
module.exports = { prompts() { return [ { name: 'name', message: 'What is the name of the new project', default: this.outFolder, filter: val => val.toLowerCase() } ] }, actions: [ { type: 'add', files: '**' }, { type: "move", patterns: { "LICENSE_*": "LICENSE" } } ], async completed() { this.gitInit() await this.npmInstall() this.showProjectTips() } }
@yami-beta/create-ts-app
では このような実装 になっています。
一部を抜粋すると、以下のようにコマンド実行時の回答に応じて package.json に記載する依存関係を編集することも可能です。const config = { actions() { const { answers } = this; return [ // 略 { type: "modify", files: "package.json", handler(data: any, filepath: string) { return { name: answers.name || data.name, version: answers.version || data.version, main: data.main, author: answers.author, license: answers.license || data.license, scripts: data.scripts, dependencies: { ...data.dependencies }, devDependencies: { ...data.devDependencies, "@typescript-eslint/eslint-plugin": answers.features.includes( "eslint" ) ? data.devDependencies["@typescript-eslint/eslint-plugin"] : undefined, "@typescript-eslint/parser": answers.features.includes("eslint") ? data.devDependencies["@typescript-eslint/parser"] : undefined, eslint: answers.features.includes("eslint") ? data.devDependencies["eslint"] : undefined, "eslint-config-prettier": answers.features.includes("eslint") && answers.features.includes("prettier") ? data.devDependencies["eslint-config-prettier"] : undefined, "eslint-plugin-prettier": answers.features.includes("eslint") && answers.features.includes("prettier") ? data.devDependencies["eslint-plugin-prettier"] : undefined, prettier: answers.features.includes("prettier") ? data.devDependencies["prettier"] : undefined } }; } }, // 略 ].filter(Boolean); } };CLI を作るほどでもない場合
ボイラープレートは欲しいけれども CLI を作るほどでは無い、という場合もあるかと思います。
そういう場合は GitHub のテンプレートリポジトリでボイラープレートを活用する方法があります。
- https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository
- https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template
詳細は上記のドキュメントを参照してください。
まとめ
- React アプリケーションのボイラープレートを生成する CLI を作っている
- テンプレートからファイルをコピー、リネーム、編集することが出来るので複数のボイラープレート生成が可能
- 手軽にボイラープレートを作る場合は GitHub のテンプレートリポジトリが活用出来そう
備考
- 投稿日:2019-12-04T10:04:28+09:00
C10K問題とNode.js
C10K問題(クライアント1万台問題)
- アクセスするクライアント数が1万を超えると、サーバーのスレッド(並列処理の単位)数が増え、サーバーのメモリーなどのリソースが不足してしまう問題
- 処理能力に余裕があっても、クライアントの数が多くなると効率が悪化しサーバがパンクする
- プロセッサの処理能力には余裕があっても、サーバの台数を増やさなければいけなくなってしまう
回避方法
- サーバーサイドではイベント駆動方式を利用しているNode.jsなどを使用する
- イベント駆動により大量のリクエストを同時に処理できるスケーラビリティを備えている
- ノンブロッキングI/Oモデルにより、C10K問題に対応する
Node.jsとは
スケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動のJavaScript環境
Node.js
- それぞれの意味
- スケーラブル : 拡張性が高い
- 非同期 : 各要求(request)の処理が完了するのを待たずに、それ以降の処理を行う方式
- イベント駆動 : イベントと呼ばれるアプリや端末上で起きた出来事に対して処理を行うプログラムの実行形式
- 特徴
- サーバーサイドで使用できる
- ノンブロッキングI/Oモデルを採用しており、I/Oの処理を待たずに次の処理を始めることができるので、大量のデータ処理が可能
- ノンブロッキング : ある処理を行いながら、ほかの処理も同時進行で行えること
- I/O : Input/Outputの略で、入出力の意
参照
- 投稿日:2019-12-04T08:03:58+09:00
Microsoft Custom Vision Serviceによる中耳炎画像認識LINE Botの作成
概要
プログラムの勉強を始めて5か月ほどの開業医です。
前回、Microsoft Custom Vision Service を使用して鼓膜画像認識を試し、極めて高い診断精度でした。
Microsoft Custom Vision Service を使用した鼓膜画像認識前回は「正常鼓膜」か、「急性中耳炎」か、「滲出性中耳炎」かを分けるためのタグだけでしたが、今回は急性中耳炎の重症度を判定できるようにするため「鼓膜の発赤の程度」、「鼓膜の腫脹の程度」、「耳漏の有無」に関するタグに追加しました。
さらに、LINE Botと連携しNowでデプロイしました。
実装
スマホから鼓膜の写真をLINE Bot宛てに送ると、中耳炎かどうか応えてくれるLINE Bot。
概念図
動作確認
中耳炎 AI診断Bot https://t.co/Rp7CU1ENos @YouTubeより
— 病気のセルフチェック (@Selfcheckhealt1) December 3, 2019作成方法
1.タグの付けなおし
以下のようにタグを付けなおしていきます。
正常鼓膜は「正常鼓膜」「発赤:なし」「腫脹:なし」「耳漏:なし」のタグを、
滲出性中耳炎は「滲出性中耳炎」「発赤:なし」「腫脹:なし」「耳漏:なし」のタグを、
急性中耳炎は「急性中耳炎」そして鼓膜発赤の程度により「発赤:なし」「発赤:一部」「発赤:全体」のタグを、鼓膜腫脹の程度により「腫脹:なし」「腫脹:一部」「腫脹:全体」のタグを、耳漏の有無により「耳漏:なし」「耳漏:あり」のタグを付けました。2.再トレーニング
タグが多くなったためか全体の精度が落ちました。
正確な判定のためにはタグ毎に最低30枚の画像が必要なようですが、一部30枚未満のタグができてしまいました。
3.テスト
テストデータ30枚をテストします。
「正常鼓膜」か「急性中耳炎」か「滲出性中耳炎」かの診断は前回同様100%正解しました。
急性中耳炎の重症度判定に使用する「発赤の程度」「腫脹の程度」は間違っているところがありました。4.LINE Bot との連携
Azure Custom Vision ServicesのPerformanceからPublishをクリックし、Prediction APIを発行します。
「If you have an image file:」のURLと
「Set Prediction-Key Header to :」のKeyを後で使うのでひかえておきます。5.LINE BoTの作成
こちらの記事を参考にしました。
はやい!やすい!うまい!Custom Vision と LINE bot でお寿司の判定をしてみた
LINE動物図鑑の作り方このようなコードを書きました。
'use strict'; const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const fs = require('fs'); const bodyParser = require('body-parser'); const Request = require('request'); const cv = require('customvision-api'); const config = { channelSecret: '自分のchannelSecret', channelAccessToken: '自分のchannelAccessToken' }; const app = express(); app.use(bodyParser.json()); let middle = line.middleware(config); const client = new line.Client(config); app.post('/webhook', (req, res) => { console.log(req.body.events); if(req.body.events[0].message.type !== 'image') return; // ユーザーがLINE Bot宛てに送った写真のURLを取得する const options = { url: `https://api.line.me/v2/bot/message/${req.body.events[0].message.id}/content`, method: 'get', headers: { 'Authorization': 'Bearer 自分のchannelAccessToken' , }, encoding: null }; Request(options, function(error, response, body) { if (!error && response.statusCode == 200) { //保存 console.log(options.url + '/image.jpg'); let strURL = options.url + '/image.jpg'; //Nowでデプロイする場合は、/tmp/のパスが重要 fs.writeFileSync(`/tmp/` + req.body.events[0].message.id + `.png`, new Buffer(body), 'binary'); const filePath = `/tmp/` + req.body.events[0].message.id + `.png`; //Azure Custom Vision APIの設定 const config = { "predictionEndpoint": "ひかえておいたURL", "predictionKey": 'ひかえておいたKey' }; cv.sendImage( filePath, config, (data) => { console.log(data); let Probability0 = data.predictions[0].probability * 100; let Probability1 = data.predictions[1].probability * 100; let Probability2 = data.predictions[2].probability * 100; let Probability3 = data.predictions[3].probability * 100; let Probability4 = data.predictions[4].probability * 100; let strName0 = data.predictions[0].tagName; let strProbability0 = Probability0.toFixed(); let strName1 = data.predictions[1].tagName; let strProbability1 = Probability1.toFixed(); let strName2 = data.predictions[2].tagName; let strProbability2 = Probability2.toFixed(); let strName3 = data.predictions[3].tagName; let strProbability3 = Probability3.toFixed(); let strName4 = data.predictions[4].tagName; let strProbability4 = Probability4.toFixed(); client.replyMessage(req.body.events[0].replyToken, { type: 'text', text:strName0 + ':'+strProbability0+'%,\n'+ strName1 + ':'+strProbability1+'%,\n'+ strName2 + ':'+strProbability2+'%,\n'+ strName3 + ':'+strProbability3+'%\n'+ strName4 + ':'+strProbability4+'%' //実際に返信の言葉を入れる箇所 }); try { fs.unlinkSync(filePath); return true; } catch(err) { return false; } return; }, (error) => { console.log(error) } ); } else { console.log('imageget-err'); } }); }); (process.env.NOW_REGION) ? module.exports = app : app.listen(PORT); console.log(`Server running at ${PORT}`);6.Nowでデプロイ
こちらの記事を参考にしました。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest考察
結構簡単にAIによる画像認識モデルとLINE BoTを連携できました。
今後は重症度判定に必要なタグを含んだ急性中耳炎の画像を増やし、精度を上げていきたいと思います。
そして以前作った中耳炎診療ガイドラインに沿った診断や治療選択ができるBOT
急性中耳炎診断支援LINE Botを改良しHerokuにデプロイ
に組み込んで、質問に返答し鼓膜の画像を送れば、自動で診断や治療方針が決定させるBOTを作成したいと思います。
- 投稿日:2019-12-04T08:03:58+09:00
AIによる中耳炎画像認識LINE Botの作成
概要
プログラムの勉強を始めて5か月ほどの開業医です。
前回、Microsoft Custom Vision Service を使用して鼓膜画像認識を試し、極めて高い診断精度でした。
Microsoft Custom Vision Service を使用した鼓膜画像認識前回は「正常鼓膜」か、「急性中耳炎」か、「滲出性中耳炎」かを分けるためのタグだけでしたが、今回は急性中耳炎の重症度を判定できるようにするため「鼓膜の発赤の程度」、「鼓膜の腫脹の程度」、「耳漏の有無」に関するタグに追加しました。
さらに、LINE Botと連携しNowでデプロイしました。
実装
スマホから鼓膜の写真をLINE Bot宛てに送ると、中耳炎かどうか応えてくれるLINE Bot。
概念図
動作確認
中耳炎 AI診断Bot https://t.co/Rp7CU1ENos @YouTubeより
— 病気のセルフチェック (@Selfcheckhealt1) December 3, 2019作成方法
1.タグの付けなおし
以下のようにタグを付けなおしていきます。
正常鼓膜は「正常鼓膜」「発赤:なし」「腫脹:なし」「耳漏:なし」のタグを、
滲出性中耳炎は「滲出性中耳炎」「発赤:なし」「腫脹:なし」「耳漏:なし」のタグを、
急性中耳炎は「急性中耳炎」そして鼓膜発赤の程度により「発赤:なし」「発赤:一部」「発赤:全体」のタグを、鼓膜腫脹の程度により「腫脹:なし」「腫脹:一部」「腫脹:全体」のタグを、耳漏の有無により「耳漏:なし」「耳漏:あり」のタグを付けました。2.再トレーニング
タグが多くなったためか全体の精度が落ちました。
正確な判定のためにはタグ毎に最低30枚の画像が必要なようですが、一部30枚未満のタグができてしまいました。
3.テスト
テストデータ30枚をテストします。
「正常鼓膜」か「急性中耳炎」か「滲出性中耳炎」かの診断は前回同様100%正解しました。
急性中耳炎の重症度判定に使用する「発赤の程度」「腫脹の程度」は間違っているところがありました。4.LINE Bot との連携
Azure Custom Vision ServicesのPerformanceからPublishをクリックし、Prediction APIを発行します。
「If you have an image file:」のURLと
「Set Prediction-Key Header to :」のKeyを後で使うのでひかえておきます。5.LINE BoTの作成
こちらの記事を参考にしました。
はやい!やすい!うまい!Custom Vision と LINE bot でお寿司の判定をしてみた
LINE動物図鑑の作り方このようなコードを書きました。
'use strict'; const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const fs = require('fs'); const bodyParser = require('body-parser'); const Request = require('request'); const cv = require('customvision-api'); const config = { channelSecret: '自分のchannelSecret', channelAccessToken: '自分のchannelAccessToken' }; const app = express(); app.use(bodyParser.json()); let middle = line.middleware(config); const client = new line.Client(config); app.post('/webhook', (req, res) => { console.log(req.body.events); if(req.body.events[0].message.type !== 'image') return; // ユーザーがLINE Bot宛てに送った写真のURLを取得する const options = { url: `https://api.line.me/v2/bot/message/${req.body.events[0].message.id}/content`, method: 'get', headers: { 'Authorization': 'Bearer 自分のchannelAccessToken' , }, encoding: null }; Request(options, function(error, response, body) { if (!error && response.statusCode == 200) { //保存 console.log(options.url + '/image.jpg'); let strURL = options.url + '/image.jpg'; //Nowでデプロイする場合は、/tmp/のパスが重要 fs.writeFileSync(`/tmp/` + req.body.events[0].message.id + `.png`, new Buffer(body), 'binary'); const filePath = `/tmp/` + req.body.events[0].message.id + `.png`; //Azure Custom Vision APIの設定 const config = { "predictionEndpoint": "ひかえておいたURL", "predictionKey": 'ひかえておいたKey' }; cv.sendImage( filePath, config, (data) => { console.log(data); let Probability0 = data.predictions[0].probability * 100; let Probability1 = data.predictions[1].probability * 100; let Probability2 = data.predictions[2].probability * 100; let Probability3 = data.predictions[3].probability * 100; let Probability4 = data.predictions[4].probability * 100; let strName0 = data.predictions[0].tagName; let strProbability0 = Probability0.toFixed(); let strName1 = data.predictions[1].tagName; let strProbability1 = Probability1.toFixed(); let strName2 = data.predictions[2].tagName; let strProbability2 = Probability2.toFixed(); let strName3 = data.predictions[3].tagName; let strProbability3 = Probability3.toFixed(); let strName4 = data.predictions[4].tagName; let strProbability4 = Probability4.toFixed(); client.replyMessage(req.body.events[0].replyToken, { type: 'text', text:strName0 + ':'+strProbability0+'%,\n'+ strName1 + ':'+strProbability1+'%,\n'+ strName2 + ':'+strProbability2+'%,\n'+ strName3 + ':'+strProbability3+'%\n'+ strName4 + ':'+strProbability4+'%' //実際に返信の言葉を入れる箇所 }); try { fs.unlinkSync(filePath); return true; } catch(err) { return false; } return; }, (error) => { console.log(error) } ); } else { console.log('imageget-err'); } }); }); (process.env.NOW_REGION) ? module.exports = app : app.listen(PORT); console.log(`Server running at ${PORT}`);6.Nowでデプロイ
こちらの記事を参考にしました。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest考察
結構簡単にAIによる画像認識モデルとLINE BoTを連携できました。
今後は重症度判定に必要なタグを含んだ急性中耳炎の画像を増やし、精度を上げていきたいと思います。
そして以前作った中耳炎診療ガイドラインに沿った診断や治療選択ができるBOT
急性中耳炎診断支援LINE Botを改良しHerokuにデプロイ
に組み込んで、質問に返答し鼓膜の画像を送れば、自動で診断や治療方針が決定させるBOTを作成したいと思います。
- 投稿日:2019-12-04T06:29:52+09:00
NestJSで始めるGraphQLサーバ開発(コードファースト編)
NestJSは、TypeScriptで記述するバックエンドアプリケーションフレームワークです。デフォルトで DI(Dependency Injection) の仕組みをサポートしており、テスト可能な構成を簡単に作ることができる特徴があります。
今回の記事ではNestJSを使用して最もシンプルなGraphQLサーバを構築します。
↓完成イメージ
GraphQLの基本
GraphQLは、RESTエンドポイントのように煩雑に管理されたエンドポイントではなく、1つのエンドポイントに対して厳密に型指定されたスキーマとしてAPIを実行します。
GraphQLについて深くは解説しませんが、以下のリンクがとても参考になります。初学者は一読しておくことをオススメします。
NestJSでGraphQL
NestJSを使用したGraphQLの開発には2つの方法があります。
- スキーマファースト
- コードファースト
スキーマファーストのアプローチでは GraphQL SDL(スキーマ定義言語)をもとにしてTypeScript定義を自動的に生成します。
一方でコードファーストのアプローチでは、デコレータとTypeScriptのクラスのみを使用して対応する GraphQL スキーマを生成します。今回はコードファーストのアプローチでGraphQLサーバを作成していきます。
まず始めに nestjsのコマンドラインツール@nestjs/cli
をインストールしましょう。インストールができたら nest コマンドが使用できます。早速 NestJSアプリケーションを作成します。$ npm i -g @nestjs/cli $ nest new nest-graphql作成されたNestJSアプリケーションを起動しましょう。
$ cd nest-graphql/ $ npm run start > nest-graphql@0.0.1 start /Users/daisuke/work/nest-graphql > ts-node -r tsconfig-paths/register src/main.ts [Nest] 5868 - 2019-12-03 21:36:33 [NestFactory] Starting Nest application... [Nest] 5868 - 2019-12-03 21:36:33 [InstanceLoader] AppModule dependencies initialized +28ms [Nest] 5868 - 2019-12-03 21:36:33 [RoutesResolver] AppController {/}: +10ms [Nest] 5868 - 2019-12-03 21:36:33 [RouterExplorer] Mapped {/, GET} route +16ms [Nest] 5868 - 2019-12-03 21:36:33 [NestApplication] Nest application successfully started +6msブラウザで localhost:3000 にアクセスして Hello Wold! が表示されれば準備OKです。
この状態ではまだRESTAPIの形式になっていますね。
GraphQL 関連ライブラリのインストール
GraphQLサーバを実装していきますので、まずは必要なライブラリをインストールします。
$ npm i --save @nestjs/graphql \ apollo-server-express \ graphql-tools \ graphql \ type-graphqlREST API用に作られていた app.module.ts を書き換えましょう。
Controller, Service の箇所を GraphQLModule として書き換えました。
.forRoot()
メソッドでplayground: true
を宣言することで ブラウザ(http://localhost:3000/graphql)で GraphQL IDEを表示できます。autoSchemaFile
は自動的に生成されたスキーマが作成されるパスを示していますapp.module.tsimport { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; @Module({ imports: [ GraphQLModule.forRoot({ playground: true, autoSchemaFile: 'schema.graphql' }), ], }) export class AppModule {}playground はアプリケーションがバックグラウンドで実行されている間に、Webブラウザーを開いて http://localhost:3000/graphql にアクセスすると表示できます。
npm run start
を実行してアプリケーションを起動してからブラウザを開いてみましょう。$ npm run start > nest-graphql@0.0.1 start /Users/daisuke/work/nest-graphql > ts-node -r tsconfig-paths/register src/main.ts [Nest] 8832 - 2019-12-03 23:44:15 [NestFactory] Starting Nest application... [Nest] 8832 - 2019-12-03 23:44:15 [InstanceLoader] AppModule dependencies initialized +26ms [Nest] 8832 - 2019-12-03 23:44:15 [InstanceLoader] RecipesModule dependencies initialized +1ms [Nest] 8832 - 2019-12-03 23:44:15 [InstanceLoader] GraphQLModule dependencies initialized +0ms [Nest] 8832 - 2019-12-03 23:44:15 [NestApplication] Nest application successfully started +82msModuleを作成
NestJSの流儀に従って、まずはModuleを作成します。例としてレシピの一覧が表示できるアプリケーションを想定しています。
$ nest generate module recipes CREATE /src/recipes/recipes.module.ts (84 bytes) UPDATE /src/app.module.ts (325 bytes)app.module.ts に自動的に RecipesModule が追加されるので確認しておきましょう。
import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { RecipesModule } from './recipes/recipes.module'; @Module({ imports: [ GraphQLModule.forRoot({ playground: true, autoSchemaFile: 'schema.graphql', }), RecipesModule, // <-- 自動的に追加される ], }) export class AppModule {}Modelを作成
次に Model を作成します。
type-graphql のライブラリから各種デコレータで宣言するものを import します。$ nest generate class recipes/recipe CREATE /src/recipes/recipe.spec.ts (147 bytes) CREATE /src/recipes/recipe.ts (23 bytes)import { Field, ID, ObjectType } from 'type-graphql'; @ObjectType() export class Recipe { @Field(type => ID) id: string; @Field() title: string; }Resolverを作成
最後にクエリの操作を行うリゾルバを作成します。
$ nest generate resolver recipes CREATE /src/recipes/recipes.resolver.spec.ts (477 bytes) CREATE /src/recipes/recipes.resolver.ts (98 bytes) UPDATE /src/recipes/recipes.module.ts (170 bytes)このResolverに Query、Mutation、Subscriptionを実装していきます。
今回は簡単のため、データベースには接続せずにレシピの一覧を返却する処理(Query)を実装しています。import { Resolver, Query, Args } from '@nestjs/graphql'; import { Recipe } from './recipe'; const recipeTable = [ { id: '1', title: '鯖の味噌煮', }, { id: '2', title: 'ミートソーススパゲティ', }, { id: '3', title: '豚の生姜焼', }, ]; @Resolver('Recipes') export class RecipesResolver { @Query(returns => [Recipe]) async recipes(): Promise<Recipe[]> { return recipeTable; } }ここまででディレクトリ構成は以下のようになっています。
src$ tree -L 2 . ├── app.module.ts ├── main.ts └── recipes ├── recipe.spec.ts ├── recipe.ts ├── recipes.module.ts ├── recipes.resolver.spec.ts └── recipes.resolver.tsスキーマの作成
あとはアプリケーションを起動するとスキーマが自動的に作成されます。
$ npm run startscema.graphql にスキーマが自動的に作成されています。
# ----------------------------------------------- # !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- type Query { recipes: [Recipe!]! } type Recipe { id: ID! title: String! }動作確認をしましょう。
http://localhost:3000/graphql にアクセスしてクエリを実行します。
確かにフィールドごとに選択されて Query が実行できていますね。
NestJSを使用することで、モデルに対してデコレータを付与するだけでシンプルかつ簡単に実装できました。
NestJSは適切にDIをすることでコードのテスタビリティをあげることができる強力なフレームワークです。
GraphQLサーバを組む場合にも威力を発揮できる可能性があり魅力的ですね。
- 投稿日:2019-12-04T05:35:48+09:00
SeleniumでSortableJS系ライブラリのDrag&Dropをテストする
前置き
前回の記事で、Vue.Draggableを使ったコンポーネントのドラッグ&ドロップを実行するCypressのテストコードについて書きました。
これをSeleniumで書いたらどうなるだろうと思い試してみたところCypress以上にハマったので、解決方法を記録しておきます。1本記事内のドラッグ&ドロップのテストコードは、Vue.Draggableに限らずSortableJSベースのライブラリなら概ね動くものになります。
以下の公式サイトのデモにて検証しています。(2019/12/3時点)※react-sortablejsと他の3種類とでは若干テストコードが変わります。
本文内ではSortableJSとreact-sortablejsのデモページに対するテストコードを掲載しています。使用言語はNode.jsとRubyです。環境
- OS: Mac OS X 10.14.6 Mojave
- Node.js
- Node.js: v12.13.1
- selenium-webdriver: 4.0.0-alpha.5
- Mocha: 6.2.2
- Ruby
- Ruby: 2.6.5
- selenium-webdriver: 3.142.6
- minitest: 5.13.0
- Browser
- Google Chrome: 78.0.3904.108(Official Build)
- chromedriver: 78.0.3904.105(Homebrewにてインストール)
- Firefox: 70.0.1 (64 ビット)
- geckodriver: 0.26.0(Homebrewにてインストール)
- Safari: 13.0.3
- safaridriver: 1.0
- Library(公式のデモで使用されていると思われるバージョン)
- SortableJS: 1.10.0-rc3
- Vue.Draggable: 2.23.2
- react-sortablejs: 1.5.1
- ngx-sortablejs: 3.1.3
ドラッグ&ドロップが動作するテストコード(Node.js版)
SortableJSの公式のデモページにアクセスし、Simple list example の Item 1 を Item 2 にドラッグ&ドロップして、テキストが入れ替わることを確認するテストコードです。
テストフレームワークはMochaを、アサーションはNode.jsのassertモジュールを使用しています。
マニュアル操作では以下のGIFアニメのようになります。
test.jsconst { Builder, By } = require('selenium-webdriver') const assert = require('assert') describe('Drag and Drop test', function () { // ブラウザの起動を待つあいだにMochaがタイムアウトしてしまうのを防止 this.timeout(20 * 1000) let driver beforeEach(async () => { driver = await new Builder() .forBrowser('chrome') // Chromeを使う場合 // .forBrowser('firefox') // Firefoxを使う場合 // .forBrowser('safari') // Safariを使う場合 .build() }) afterEach(async () => { await driver.quit() }) it('SortableJS', async () => { // SortableJSの公式デモページにアクセス await driver.get('https://sortablejs.github.io/Sortable/#simple-list') // ドラッグ&ドロップの対象を含むdiv要素のリストを取得 let elements elements = await driver.findElements(By.css('div#example1 > div.list-group-item')) // ドラッグ元(Item 1)とドロップ先(Item 2)のdiv要素を取得 const sourceElement = await elements[0] const targetElement = await elements[1] // ドラッグ&ドロップを実行する関数の呼び出し await simulateDragAndDrop(sourceElement, targetElement) // Item 1 と Item 2 が入れ替わったことを確認 elements = await driver.findElements(By.css('div#example1 > div.list-group-item')) assert.strictEqual(await elements[0].getText(), 'Item 2') assert.strictEqual(await elements[1].getText(), 'Item 1') }) /** * ドラッグ&ドロップを実行する関数 */ async function simulateDragAndDrop(sourceElement, targetElement) { await driver.executeScript( async args => { // dragoverイベントの発火位置を計算 const targetRect = args.targetElement.getBoundingClientRect() const targetPositionX = (targetRect.left + targetRect.right) / 2 const targetPositionY = (targetRect.top + targetRect.bottom) / 2 // ドラッグ&ドロップに必要な各イベントのインスタンスオブジェクトを作成 const pointerDownEvent = new PointerEvent('pointerdown', { bubbles: true, cancelable: true, }) const dragStartEvent = new MouseEvent('dragstart', { bubbles: true, }) const dragOverEvent = new MouseEvent('dragover', { bubbles: true, clientX: targetPositionX, clientY: targetPositionY, }) const dropEvent = new MouseEvent('drop', { bubbles: true, }) // sleep処理用の関数を定義 const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) // イベントの発火 args.sourceElement.dispatchEvent(pointerDownEvent) args.sourceElement.dispatchEvent(dragStartEvent) await sleep(1) args.targetElement.dispatchEvent(dragOverEvent) args.targetElement.dispatchEvent(dropEvent) }, { sourceElement, targetElement } ) } })テストコードの解説
SortableJSを使用した要素のドラッグ&ドロップを実行するには、以下の4つのイベントの発火が必要になります。
- pointerdown
- dragstart
- dragover
- drop
selenium-webdriver本体にもドラッグ&ドロップ機能は実装されていますし(公式ドキュメント)、ドラッグ&ドロップ操作のための外部ライブラリもいくつか公開されています。
しかし試してみた範囲では、いずれも何かしらのイベントの発火が足りずドラッグ&ドロップは期待通りに動作しませんでした。Cypressのように必要なイベントを個別に発火させることができればよさそうなのですが、selenium-webdriverにはそういった機能はないようです。
そのため、素のJavaScriptでイベントを発火させる処理を書き、それをselenium-webdriverのexecuteScript()
を使って実行するという方法をとることになりました。JavaScriptを書く際のポイントが何点かありましたので説明します。
ポイント1
dragover イベントのインスタンス作成時のコンストラクタで、イベントを発火させる位置を指定しておく必要があります。
getBoundingClientRect()
でドロップ対象要素の viewport に対する位置を取得し、それをもとに対象要素の中央にあたる位置を計算して、その値をコンストラクタのclientX
、clientY
に設定しました。test.js// dragoverイベントの発火位置を計算 const targetRect = args.targetElement.getBoundingClientRect() const targetPositionX = (targetRect.left + targetRect.right) / 2 const targetPositionY = (targetRect.top + targetRect.bottom) / 2 // 中略 // dragoverイベントのコンストラクタでイベントの発火位置を指定 const dragOverEvent = new MouseEvent('dragover', { bubbles: true, clientX: targetPositionX, clientY: targetPositionY, })ポイント2
dragstart と dragover を順に dispatchEvent する際、あいだに sleep を挟む必要があります。
sleep が必要になる根本的な理由がまだ突き止められていないのですが、ひとまず動いたのでよしとしています。test.js// sleep処理用の関数を定義 const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) // イベントの発火 args.sourceElement.dispatchEvent(pointerDownEvent) args.sourceElement.dispatchEvent(dragStartEvent) // ここでsleepが必要 await sleep(1) args.targetElement.dispatchEvent(dragOverEvent) args.targetElement.dispatchEvent(dropEvent)ポイント3
MacのSafariをテスト対象とする場合ですが、SafariではDragEvent
をnewできません。(Chrome、Firefoxではできます)
そのためドラッグ系のイベントでもMouseEvent
を使っています。
MDN にも Can I use... にもSafariはDragEventをサポートしていると書かれているのですが、Safariのコンソールで直接コードを叩いてみてもReferenceError: Can't find variable: DragEvent
と返ってきてしまいました。test.js// Safariでは new DragEvent と書くと動作しない const dragStartEvent = new MouseEvent('dragstart', { bubbles: true, }) const dragOverEvent = new MouseEvent('dragover', { bubbles: true, clientX: targetPositionX, clientY: targetPositionY, }) const dropEvent = new MouseEvent('drop', { bubbles: true, })ポイント4
前置きにも書きましたがreact-sortablejsのデモの場合、前出のテストコードではドラッグ&ドロップが動作しません。react-sortablejsでは、dragstart イベントが発火した際に、イベントターゲットとなった要素が2つに増えるという挙動をします。
この要素の増加により、リスト内でのドロップ先要素の index がずれてしまい、目的のドロップ先に dragover できなくなるケースが発生します。それに対応するため処理に手を加えなければなりません。要素数の増加に対応したテストコードの例が以下になります。
ドラッグ&ドロップが動作するテストコード(Node.js + react-sortablejs版)
react-sortablejsの公式のデモページにアクセスし、Simple List の List Item 1 を List Item 2 にドラッグ&ドロップしてテキストが入れ替わることを確認するテストコードです。
記事が長くなるので折りたたみます。
react-sortablejsのテストコード例
test.js// requireやbefore/after部分は前出のテストコードと共通 it('react-sortable', async () => { // react-sortablejsの公式デモページにアクセス await driver.get('http://sortablejs.github.io/react-sortablejs/#container') let elements, sourceElementIndex, targetElementIndex // ドラッグ&ドロップの対象を含むli要素のリストを取得 elements = await driver.findElements(By.css('ul.block-list > li')) // ドラッグ元(List Item 1)とドロップ先(List Item 2)のli要素の、リスト内でのindexを定義 sourceElementIndex = 0 targetElementIndex = 1 // ドラッグ&ドロップを実行する関数の呼び出し await simulateDragAndDropForReact(elements, sourceElementIndex, targetElementIndex) // List Item 1 と List Item 2 が入れ替わったことを確認 elements = await driver.findElements(By.css('ul.block-list > li')) assert.strictEqual(await elements[0].getText(), 'List Item 2') assert.strictEqual(await elements[1].getText(), 'List Item 1') }) /** * ドラッグ&ドロップを実行する関数 */ async function simulateDragAndDropForReact(elements, sourceElementIndex, targetElementIndex) { await driver.executeScript( async args => { // dragoverイベントの発火位置を計算 const targetRect = args.elements[args.targetElementIndex].getBoundingClientRect() const targetPositionX = (targetRect.left + targetRect.right) / 2 const targetPositionY = (targetRect.top + targetRect.bottom) / 2 // ドラッグ&ドロップに必要な各イベントのインスタンスオブジェクトを作成 const pointerDownEvent = new PointerEvent('pointerdown', { bubbles: true, cancelable: true, }) const dragStartEvent = new MouseEvent('dragstart', { bubbles: true, }) const dragOverEvent = new MouseEvent('dragover', { bubbles: true, clientX: targetPositionX, clientY: targetPositionY, }) const dropEvent = new MouseEvent('drop', { bubbles: true, }) // sleep処理用の関数を定義 const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) // ドラッグ元の要素よりもドロップ先の要素が要素リストの後ろにある場合、 // dragover発火時にイベントターゲットとなるドロップ先要素のindexを+1する const adjustIndex = args.sourceElementIndex < args.targetElementIndex ? 1 : 0 // イベントの発火 args.elements[args.sourceElementIndex].dispatchEvent(pointerDownEvent) args.elements[args.sourceElementIndex].dispatchEvent(dragStartEvent) await sleep(1) args.elements[args.targetElementIndex + adjustIndex].dispatchEvent(dragOverEvent) args.elements[args.targetElementIndex].dispatchEvent(dropEvent) }, { elements, sourceElementIndex, targetElementIndex } ) }ドラッグ&ドロップが動作するテストコード(Ruby版)
Rubyでは以下のように書くことができます。2
テストフレームワークはminitestを使用しています。記事が長くなるので折りたたみます。
Rubyのテストコード例
test.rbrequire 'selenium-webdriver' require 'minitest/autorun' describe 'Drag and Drop test' do driver = nil before do driver = Selenium::WebDriver.for :chrome # Chromeを使う場合 # driver = Selenium::WebDriver.for :firefox # Firefoxを使う場合 # driver = Selenium::WebDriver.for :safari # Safariを使う場合 end after do driver.quit end it 'SortableJS' do # SortableJSの公式デモページにアクセス driver.get 'https://sortablejs.github.io/Sortable/#simple-list' # ドラッグ&ドロップの対象を含むdiv要素のリストを取得 elements = driver.find_elements(:css, 'div#example1 > div.list-group-item') # ドラッグ元(Item 1)とドロップ先(Item 2)のdiv要素を取得 sourceElement = elements[0] targetElement = elements[1] # ドラッグ&ドロップを実行するメソッドの呼び出し simulateDragAndDrop(sourceElement, targetElement, driver) # Item 1 と Item 2 が入れ替わったことを確認 elements = driver.find_elements(:css, 'div#example1 > div.list-group-item') assert_equal(elements[0].text, 'Item 2') assert_equal(elements[1].text, 'Item 1') end end # # ドラッグ&ドロップを実行するメソッド # def simulateDragAndDrop(sourceElement, targetElement, driver) driver.execute_script(<<-EOL, sourceElement, targetElement) (async (sourceElement, targetElement) => { // dragoverイベントの発火位置を計算 const targetRect = targetElement.getBoundingClientRect() const targetPositionX = (targetRect.left + targetRect.right) / 2 const targetPositionY = (targetRect.top + targetRect.bottom) / 2 // ドラッグ&ドロップに必要な各イベントのインスタンスオブジェクトを作成 const pointerDownEvent = new PointerEvent('pointerdown', { bubbles: true, cancelable: true, }) const dragStartEvent = new MouseEvent('dragstart', { bubbles: true, }) const dragOverEvent = new MouseEvent('dragover', { bubbles: true, clientX: targetPositionX, clientY: targetPositionY, }) const dropEvent = new MouseEvent('drop', { bubbles: true, }) // sleep処理用の関数を定義 const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) // イベントの発火 sourceElement.dispatchEvent(pointerDownEvent) sourceElement.dispatchEvent(dragStartEvent) await sleep(1) targetElement.dispatchEvent(dragOverEvent) targetElement.dispatchEvent(dropEvent) })(arguments[0], arguments[1]) EOL endテスト対象がreact-sortablejsの場合は、Node.js版と同じように手を加える必要があります。(テストコード例は割愛)
後書き
個人的にはドラッグ&ドロップの挙動自体はUI観点も含めてマニュアルテストで見ておくのがよいだろうという考えでいます。
しかし、ドラッグ&ドロップ実行後の画面のテストを自動でまわしたいというケースは、もしかしたら出てくるかもしれません。そのようなときに今回調べた方法が役に立てばと思います。3
参考サイト
- Selenium公式 4
- Seleniumソースコード(Node.js) - GitHub
- Seleniumソースコード(Ruby) - GitHub
- SortableJS公式
- SortableJSソースコード - GitHub
- java - How to fire JS event in selenium? - Stack Overflow
- HTML Standard - Drag and drop
- HTML Standard - Drag and drop(非公式日本語訳)
- MouseEvent - Web API | MDN
- カスタムイベントのディスパッチ - 現代の JavaScript チュートリアル
- ES2017 async/await で sleep 処理を書く - Qiita
- 投稿日:2019-12-04T00:38:16+09:00
Angularスキル獲得のために始めたこと、始めること
お仕事だったり同期と作ったアドベントカレンダーだったりのおかげで、Angularを触る機会を得た小生でございます。
今までフロントどころか、Webアプリの制作もしたことがなかったので、これをいいことにいろいろと勉強していってる最中です。Angularを触るにあたって何を知っていたか
- HTML
- CSS
- Javascript
- Node.js
HTML、CSSはお猿さんと同じくらいの知識がありました。
JavascriptはほぼNode.js触ってから覚えた感じ。
元々プログラミング経験があったので、ここらへんはなんとか理解しつつ進めております。Angularを理解するためには
- 公式:入門チュートリアル(Angular未経験者向け)
- 公式:基礎チュートリアル(入門チュートリアルをやった人向け)
- 2020年のフロントエンドマスターになりたければこの9プロジェクトを作れ(公式チュートリアルを終えて基礎がわかった人向け)
特に2020年のフロントエンドマスターになりたければこの9プロジェクトを作れはめちゃくちゃ面白いです。
Angularに限らず、フロントのフレームワークの基礎押さえたなら、それぞれ作っていくべきだと思います。
元記事ではフロントエンドマスターになるために様々なフレームワークを紹介していますが、まずは一本極めていくのが自分のやり方なので、Angularで絞ってやっていきます。始めたこと:公式チュートリアル制覇
入門チュートリアルでは、Angularがどんな感じで動いているのかを理解できました。
基礎チュートリアルでは、コンポーネント指向に置いて説明がされている印象を受けました。
コピペだけで作れなくはないですが、用語が分からずともしっかり説明を読んで、ちょこちょこコードをいじったりするとより理解が深まります。始めたこと:Build a movie search app
とっかかりとして、Angularで映画情報を検索するWebアプリを作りました。できたものはこんな感じです。
ガッツリ参考URL載せてるくせに、実は一度も読みに行ってません…
貼られてたスクショを元に、機能を想像しながら、真似た物を作ってみました。検索フォームにキーワードを入力すると…
関連する映画が表示されます。
ページ移動とかもちゃんと機能します。どれでもいいので映画をクリックすると、
このような形で、映画の詳細情報がでます。
Angularの勉強は楽しいのですが、なにぶんCSSをしっかり書いたことがないもので…
詳細情報ページだけ、間に合わせのtableで凌いでます。
(検索フォームはなぜか真ん中に来ないのでおこです?)始めたこと:AngularでWebアプリを設計するには
もうこれはWebデザイン全般に言えることかもしれないんですけど、
設計図を書きましょう もっというと、画面図を書きましょうですねAngularはコンポーネント指向でアプリを作るので、どのコンポーネントがどの部分に来るか、明確にイメージしていないとすぐこんがらがります(一人で作る場合)
最初に必要なコンポーネントをがーっと作って、その後設計を考えながら組み立てるのも悪くないですが、あとからたくさん修正が必要そうになるので、概要くらいは決めておいた方が良いです。始めること:アウトプット、アウトプット、アウトプット
やっぱり手を動かさないと始まらない、ということで当面の目標はサンプルアプリを作り続けるです。
嬉しいことにコードを書くスピードが上がっているのを実感できているので、アドベントカレンダー最終日までにあと2つはサンプルを作りたいと思います。
併せて、Bootstrapについても勉強を始めようと思います。
とりあえず次回の記事は、今回紹介したサンプルアプリの詳細と、次に作るアプリの設計について書いていきます。