ナチュラル @rch850

ナチュラル丼発祥の地、福井からお届けします。技術的な話題とか、雑談とか。

もしかして頻尿で Mashup Battle 1stStage in 北陸敗退しました

トイレに行く回数が気になったので、トイレに行った回数を記録するためのデバイスを作ってみました。

磁石とリードスイッチを使ってチャックの開閉を検出し、データをサーバに蓄積する仕組みです。

hacklog.jp

で、これを持って Mashup Awards 2017 の Mashup Battle 1stStage in 北陸 に出てみました。

@運営さん たくさんの写真、動画をありがとうございました!

昨年の Internet of Tairyoku では惜しくも2位だったので、今年こそ、と思って挑みましたが、得票は1票。無念の敗退でした。

この作品について、コンセプト自体はだいたい実現できたので満足したのですが、実装に改善の余地がありありだなと感じています。ということで、ここでひとつふりかえり。

よかった事:

  • ESP32 というおもちゃを手に入れた
    • センサーの入力結果に応じて自作 API 叩くとか kintone に保存するとか Slack に通知するとかできた
    • Bluetoothバイスとして動かすことができた
  • これをきっかけに工具箱、工具、材料などを買い揃えることができた
  • kintone に毎分データを送って可視化するといい感じだった
  • 頻尿だけじゃなくて尿道結石にも使えるんじゃないのという意見をもらった

今後の課題:

  • 電池で動かすつもりが、消費電力に不安があって PC から USB 給電でのデモとなった
    • 電池で1日動くようにしたい。いや、1日と言わず一週間は動かしたい
    • リチウムイオン電池を使って小型化してみたい
    • deep sleep とやらを使ってみたい
    • 消費電力をちゃんと測りたい
  • ブラウザから操作できたら素敵なのでは
    • Web Bluetooth API を使ってブラウザから通信したかった
    • ESP32 側での実装方法を調べきれずにできなかった
  • 小型化したい
    • Wio Node を買ってあるので、それを使うとか
    • Bluetooth 使うなら開発ボード使わず生の ESP32 にチャレンジ?
  • リードスイッチに固定に難あり
    • チャックに磁石をくっつけるのは、セロテープでぐるぐる巻きにすれば大丈夫だった
    • リードスイッチは、よく外れた。デモでも外れた

このように課題山積です。個人的にほしいデバイスなので(現時点で頻尿ってわけじゃないです)、今後も作り込んでいきたいなと思います。

ISUCON 7 予選敗退しました

ISUCON 5 のときと同じチーム「へしこず」で、2年ぶりの本戦出場を狙いましたが、本戦出場ラインには倍ぐらい届かず敗退しました。また来年会いましょう。

今回の主な装備

  • vim
  • kataribe
  • pt-query-digest (percona-toolkit), mysqldumpslow
  • beer (alcohol free)

序盤

予習で golang も検討したんですが、これまでの ISUCON で慣れてた ruby でやることにしました。

bundle install できないぞってことで sudo apt-get install -y ruby-bundler とか適当に叩いたのがアダ。 ~/xbuild 眺めたりして気づいたんですが ~/local/ruby/bin/bundle を使うのが正解だったようです。

まず POST /login のクエリがイケてない感じだったので、LIMIT 1 つけたり * を必要なものだけに書き換えたりして 10007 点になりました。その後何回か微調整してベンチマークして 15191 点あたりまで伸びました。このあたりはとりあえず API 1台だけでベンチマーク走らせてました。

この時点で2時45分ごろ。開始から2時間弱といったところでした。

中盤:icons をどうにかする

どうみても GET /icons/*.png なんとかしないとだめだよね。ってことで、なんとかすることに。画像を mysql から取り出して redis に入れる班と、nginx の設定いじってキャッシュさせる班のふた手に分かれて行動しました。俺たちを苦しめた icons の一部。

f:id:rch850:20171023012846p:plain

イコン画像が数百KBとか、仕事だったらありえないサイズだよねとか話してました。nginx 側は

 location ~ /(css|js|fonts|icons)/ {
        proxy_set_header Host $http_host;
                proxy_pass http://puma_app;
        proxy_ignore_headers Cache-Control;
        proxy_cache zone1;
        proxy_cache_valid any 1m;
        expires 1m;
        etag on;
        add_header Cache-Control "public";
    }

と書いてみたり、ruby 側では

configure do
  # 略
  set :static_cache_control, [:public, :max_age => 90]
end

とか

get '/icons/:file_name' do
  # 略
  etag file_name
  # 略
end

とか書いて、kataribe で見た icons の状況はこのようになりました。

Top 20 Sort By Total
Count     Total      Mean    Stddev     Min   P50.0   P90.0   P95.0   P99.0     Max   2xx   3xx  4xx  5xx  Request
 3097  6351.049  2.050710  3.120888   0.000   0.007   7.722  10.000  10.001  10.007  1646  1451    0    0  icons

100Mbps 使い切ってたネットワーク帯域にも少し余裕が出てきました。

あとは GET /fetch での SELECT COUNT(*) as cnt FROM message ... のコストが高いと考えて、件数を redis に入れるなどしました。

18:43 時点で 45124 点。他のチームが数十万点を出してて、だいぶ焦りが出てきました。

終盤:積み重ねでフィニッシュ

  • nginx → ruby (puma) をソケット経由にする
    • /etc/systemd/system/isubata.ruby.serviceExecStart = /home/isucon/local/ruby/bin/bundle exec puma -b unix:///tmp/puma.sock
    • /etc/nginx/sites-enabled/nginx.confupstream puma_app { server unix:/tmp/puma.sock; }, proxy_pass http://puma_app;
  • puma のプロセス数、スレッド数を増やす
    • /etc/systemd/system/isubata.ruby.serviceExecStart = /home/isucon/local/ruby/bin/bundle exec puma -w 2 -t 25 -b unix:///tmp/puma.sock
  • 既読メッセージIDを redis で管理する。
  • mysqld の max_connection を 1024 に。

などといった工夫を重ねて、終了直前には 125550 点まで伸ばせました。

f:id:rch850:20171023021054p:plain

感想

開始時間が遅れはしましたが、ベンチマークとダッシュボードは ISUCON 5 からの中で(自分が参加した中で)一番良かったと思います。

  • ベンチマークキューが詰まる気配がなかった
  • 「負荷レベルが上昇しました。」がエキサイティング!
  • レスポンスが遅いため負荷レベルを上げられませんでした。/message?channel_id=7466&last_message_id=0エラーが発生したため負荷レベルを上げられませんでした。2017-10-22 20:57:58.647006618 +0900 JST m=+31.152077525 リクエストがタイムアウトしました (POST /profile ) といった具体的なアドバイス

問題についても、icons をクリアしたら、さらに次の課題が出てくるような形で、なかなかのスルメゲーでした。これまでの ruby 実装とは違って unicorn ではなく puma、mysql2-cs-bind ではなく mysql2 だったので、調べ物に少し時間がかかってしまいました。

また、参加者連絡が discord になったわけですが、蓋を開けてみれば isubata ってのもクスリと来ました。

運営、出題の方々、ありがとうございました。残念ながら予選落ちしてしまいましたが、来年も参加したいです。いや、参加します。

ESP WROOM 32 でブザするまでのメモ

ちょっと作りたいおもちゃを思いついたので、@kimikato先生に何で作ったらいいかおすすめを聞いたところ ESP 32 とか 02 あたりがいいよと聞いたので、さっそく買ってみました。秋月の ESP32-DevKitC ESP-WROOM-32 開発ボードです。

f:id:rch850:20170921005931p:plain

  • Arduino 互換で開発しやすい
  • 小さい
  • これだけで WiFi 繋がっちゃう(技適通過済み)

といったところが売りらしいです。

さっそくマイクロ USB でつないで Arduino から書き込もうとしたのですが、「ツール」>「ボード」の選択肢に ESP らしきものが見当たりませんでした。GitHubarduino-esp32 のドキュメントを見たところインストール手順が書いてあったので、それに従ったところボードに ESP32 Dev Module などが出てきました。

これで書き込めると思ったのですが、

ボード/dev/cu.usbserial-A9007Ldzは利用できません

などというエラーが出て書き込めませんでした。どうも USB ドライバが必要そうです。あ、開発環境は macOS Sierra です。

この開発ボードについている USB 変換チップは CP2102 なので、「CP2102 ドライバ」と検索してヒットしたメーカーのサイトからドライバをダウンロードしてインストール。OS を再起動すると、無事に書き込めました。

Lチカで動作確認するのが普通なんでしょうが、手近にあったのがブザーだったので、刺して音がするのを確認しました。

ESP 32 手軽で楽しそうです!

生産管理部業務効率課進捗課長で MA 2nd 進出決めました

Mashup Award の福井ハッカソン予選で「生産管理部業務効率課 進捗課長」という作品を作り、最優秀賞を取り、2nd Stage 進出が決まりました。やった!先にソフトバンクロボティクス賞で名前が呼ばれたときは、あ、これ最優秀逃したかなーと思ったのですが、まさかのダブル受賞でした。

イベントの状況は公式のレポートに譲るとして、ここには自分がやったことなどを書いておきます。

自分が担当したのは、Pepper が録画、録音したデータを結合して、ハイライト動画、音声を作るところです。発表スライドの役割分担書く時に「自分は映像クリエイターで」って答えたら、他のメンバーもそれっぽい役割名で書く流れになってしまいました。

Pepper での録画、録音

Record Video ボックスで録画、Record Sound ボックスで録音しました。実際のコーディング、というかコレグラフでのボックスの配置は @pittanko_pta にやってもらいました。

動画と音声の結合

3人分の動画、音声を 1.avi、2.avi、3.avi といった名前で保存したので、それを結合しました。動画、音声の操作といえば ffmpeg。Pepper の内部で ffmpeg を叩けることが分かったので、別サーバに投げたりすることなく処理ができました。動画も音声も、結合には ffmpeg -f concat を使いました。なお出力形式は動画は mp4、音声は wav にしました。

結合した音声には ffmpeg -y -i sound_tmp.wav -i bgm.wav -filter_complex amerge -ac 2 -q:a 4 sound.wav といったコマンドで BGM もつけました。

映像と音声をくっつけず別々にしてる理由はすぐあとで書きます。

Pepper での動画、音声の再生

Play Video ボックスで動画を、Play Sound ボックスで音声を再生しました。最初は音声がついた動画を Play Video で再生するだけでいいと考えたのですが「Pepper 動画 音声」で調べたところ動画に音声を乗せるとノイズが入ることがあるという話を見かけたので、デモでの成功率を考えて個別に扱うことにしました。こちらもボックスの配置は @pittanko_pta の担当だったのですが、中身の調査は自分がやりました。

中身の調査というのは、作成した動画をどうやったら Play Video で再生できるのかという話です。ボックスのコードを読んだところ、ローカルのファイルは "http://%s/apps/%s" % (self.tabletService.robotIp(), subpath.replace(os.path.sep, "/")) といった形の URL で読み込んでいるようで、この apps は一体どこなのかを探す必要がありました。Pepper の中身を ps コマンドで見たところ、http サーバは nginx だったので、/etc/nginx/nginx.conf を見て apps が /opt/aldebaran/www/apps を指していることがわかりました。というわけで再生する動画はそのパスに置くことにしました。たしか nao ユーザーで書き込める場所だったはずです。Pepper の中に ssh できるのは便利ですね。

Play Sound のほうは普通にパスを指定すれば再生できたので、特に困りませんでした。

Play Video と Play Sound を同時に動かすと、Play Video のほうが数秒遅れて再生が始まるので、絶妙なウェイトを入れて同時に再生されるような工夫もしています。

まとめ

以上が映像クリエイターの仕事でした。ハイライト動画にイントロを付けたいとか、テロップを入れたいとか、いろいろやりたいことがあるので、今後もネタに困ることはなさそうです。

Angular の routerLink でクエリパラメータを指定する

Angular 4.2.2 の話です。

<a routerLink="/foo?bar=10">link</a>

のように routerLink にクエリパラメータを直接指定すると Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'foo%3Fbar%3D10' といったエラーが出ます。

RouterLink のリファレンスにあるように [queryParams] にオブジェクトを指定すれば動作するようになります。

<a routerLink="/foo" [queryParams]='{ bar: 10 }'>link</a>

routerLink がダメなら href で……というのも試してみたのですが、これだと遷移するにはするのですが、ページ全体の再読込がかかってしまいました。

<a href="/foo?bar=10">link</a>

基本的にはちゃんと動く書き方をすればいいのですが、ウェブ API から受け取ったリンク先に遷移させたい場合は、API から受け取った文字列をパースして routerLinkqueryParams に設定するといった実装が必要になってきます。これは手間です。このあたりを賢くやってくれる方法があればいいのですが。

Chrome 59 で window.open の挙動が変わった

JavaScript で新しいウィンドウを開くため、このようなコードを書いていたのですが、Chrome 59 になってから新しいタブで開くようになってしまいました。

window.open('http://example.com/', '_blank', 'width=640, height=480, location=yes')

window.open の第3引数 feature から、アドレスバーを表示するためのオプション location=yes を取り除くことで、タブではなくウィンドウで開くようになりました。

window.open('http://example.com/', '_blank', 'width=640, height=480')

こうなった原因を探るため Chrome 59 のコミットログwindow.open で検索したところ window.open() should gate new tab/new popup based on toolbar visibility. (e507bb3) が関係してそうでした。

window.open() should gate new tab/new popup based on toolbar visibility.

Previously, Chrome required that toolbar, menubar, scrollbars, status, resizable were all set to enabled to open a window as a new tab rather than a new popup. However, this causes developer frustration if one of window features is accidentally omitted (as it then defaults to disabled).

Instead, just use toolbar visibility to determine whether or not window.open() creates a new popup or a new tab, which matches Firefox.

なるほど。toolbar についても調べたところ、確かに設定次第でポップアップかどうかが変わりました。

// 新しいタブになる
window.open('http://example.com/', '_blank', 'width=640, height=480, toolbar=yes')

// 新しいウィンドウ(ポップアップ)になる
window.open('http://example.com/', '_blank', 'width=640, height=480, toolbar=no')
window.open('http://example.com/', '_blank', 'width=640, height=480')

Chrome 59 のソースを調べたことのメモ

どこかに location を toolbar として見るコードがあるはずですが、見つけられませんでした。


(6月20日追記)

location=yes の有無でどうなるか、IE 11, Edge, 14, Chrome 59, Firefox 54 で動作確認しました。

codepen.io

結果はリンク先を見ての通りですが、特に挙動が違ったところとして IE 11 だけ location=yes の時にアドレスバーの中身を編集できました。

oEmbed の height null について

きっかけは mastodon の URL 貼り付けを確認してたときにびろーんと伸びてしまうのに気づいたこと。

friends.nico

引用ここまで。めっちゃ改行入れてるわけじゃなくて、ここまでびろーんって伸びちゃってるんです。

逆に、長いトゥートは途切れてしまう。

pawoo.net

なぜこうなるか mastodon のコードを追ってみた。対象はタグ v1.3.2 のもの。

oEmbed の実装は oembed_controller.rb にあって、特別な指定がなければ height が 640 となる。

  def show
    @stream_entry = stream_entry_from_url(params[:url])
    @width        = params[:maxwidth].present?  ? params[:maxwidth].to_i  : 400
    @height       = params[:maxheight].present? ? params[:maxheight].to_i : 600
  end

https://github.com/tootsuite/mastodon/blob/v1.3.2/app/controllers/api/oembed_controller.rb#L9

height というのは oEmbed の仕様にあって rich タイプでは必須となっている。

height (required)

The height in pixels required to display the HTML.

この高さが 640 と固定されているので、びろーんと伸びてしまったり、途切れてしまったりするわけだ。

Twitter がどうしているか調べてみたら、height には null が入っていた。instagram も同様に null だった。

じゃぁ mastodon でも null 返せばいいのか?ってことで、自分の mastodon インスタンスで show メソッドを書き換えて null になるようにしたら、いい感じにフィットするようになった。

mastodon.850mb.net

なるにはなったけど required って明記されているものに対して null を返すのはちょっと抵抗あるなー。本家に PR 出しても苦い顔されそうだし、自分がその立場なら苦い顔しそう。

なお今回の調査では iframely のデバッガにとてもお世話になりました。

(5/9 追記) 出すだけ出してみようってことで PR 出したらすぐマージされましたとさ。ウワサには聞いてたけど対応速かった!

github.com