/lost+found/amanoese

作られては忘れられていくコードや日常のための日記

フェリーハッカソン2019に参加しました。ので開発過程とか書いたプログラムの解説

1/25~1/27に Code for OSAKA さん主催の フェリーハッカソン2019 に参加してきました。
中々過酷な日程できつかったのですが、過酷な日程を支援し運営に携わった方々や協賛企業の方々に感謝致します。

良いチームに恵まれたこともあり、最優秀賞港湾局賞の2つを受賞しました。嬉しかったです。 また、AP通信の津田さんに日経電子版の記事にしていただけました。ありがとうございます。

style.nikkei.com

記事中の「25歳 エンジニア担当」が自分ですね。

コンセプトとか

コンセプトなどは発表動画を見ていただけると幸いです。17分20秒くらいから説明始まります。


フェリーハッカソン 成果発表その1

チームで作ったもの


フェリーハッカソン2019 プロダクトデモ動画

あとは詳細な内容とかです。

開発過程と自分が担当したプログラムの解説をしていきます。

自分が担当したところ

f:id:amanoese:20190209153221p:plain

1日目

個人的にはMRグラスできる釣りゲーム作りたかったのですが、
アイディアのクソさや開発難易度の高さ、最後に人望が乏しさからも合わさり人は集まりませんでした。

なんやかんやで、フェリー内に水族館を作ることで意気投合していたチームに拾っていただきました。

Unityがわからない

チーム結成後に部屋が割り当てられ、快適なフェリーの部屋で今後の方針を決めました。

翌日の午前中に水族館に行くこととが決定します。
また、Unityが得意なエンジニアがいることもあり、水族館の映像や動きなどはUnityで作ると決まりました。

開発方針は下記のようなものだった記憶です。

  1. 水族館の映像とかモーションをUnityで作る。
  2. 映像の魚に触れると魚が動く
  3. 映像の魚に触れると、魚の情報がポップアップされて表示される。
  4. 魚に触れるために距離センサを使う。
  5. 船の傾きをジャイロセンサで取り込み映像を揺らす(時間がなく断念)

自分はWeb系のエンジニアでかつUnityも触ったことが無かったため、
直接手伝えることが少なかったのでUnity外で「3.」 「4.」に関するプログラムを作り連携させることを提案します。

しかし、知識不足からUnityと外部プログラムとどうやってデータをやり取りする方法がわかなかったのですが、
マウスのクリックイベントで「4.」の機能を強引に実装することと、
「3.」の魚の情報をポップアップさせる機能を、Unityの画面にオーバーレイさせたプログラムで実現させることを提案しその開発を始めます。

f:id:amanoese:20190209000411p:plain

念押しですが、このプログラムだけは本番のプロダクトに一切関係ないものです。
一応こんな構想で作ってましたというだけです。

話し合いのあとに開発を開始したのですが、
1日目で距離センサやジャイロセンサなどのハードウェアがまだなかったので、
実装が容易そうな「3.」の機能を実装しました。

github.com

f:id:amanoese:20190209014547g:plain

デザイン以外の機能は完成したのですが、このとき作った画面は残念ながらボツになります。
不幸中の幸いか後々テストツールとして活躍することになります。

2日目

2日目は別府についてすぐの港で、チームメンバーのコートが紛失するハプニングがあましたが、
運営の方が忘れ物として管理していてくれたらしく大きな問題は起きませんでした。

コートの無事が確認できたため、水族館に向かいました。

雪が降っていて寒かったです。

f:id:amanoese:20190209005557j:plain

個人的にはエビがターン制ストラテジー風に動いていたのが興味深かったです。

センサないのでUnityにアクションするプログラム作る

水族館を堪能したので、場所を開発場所が提供されている「かんぽの宿別府」に移動します。
とりあえずここで、「3.」の魚の情報をポップアップさせる機能はUnityで作ることに変更になり、1日目のでプログラムは没になります。

またセンサについては、はんだ付けが終わっていなかったため、
この時点ではまだセンサを利用した作業は行なえませんでした。

しかたないので「4.」の機能を実現するために、
任意の位置情報をHTTP通信で受け取りマウスを操作するプログラムを Node.js で作ることにしました。
HTTP通信にしているのはプロジェクタとセンサが遠くなることを想定して、サーバーを経由して操作することを想定していたためです。

f:id:amanoese:20190209013530p:plain

RobotJSというライブラリを使ったのですが、
これがとても優秀なライブラリで簡単に実装が終わりました。

index.js

// Move the mouse across the screen as a sine wave.
const express = require("express");
const app = express()
const robot = require("robotjs");

app.get('/:x/:y', (req, res) => {
  let {x,y} = req.params
  if(x == null || y == null) {return}

  robot.moveMouse(x,y);
  robot.mouseClick();
  console.log('click',{x,y})
  res.send(`click x:${x} y:${y}\n`)
})

app.listen(5000, () => console.log('Example app listening on port 5000!'))

適当にテストしてみます。

$ sleep 3;curl localhost:4000/600/600;sleep 0.5;curl localhost:4000/1200/1200

自動でマウスが操作されてるのが確認できます。

f:id:amanoese:20190209021049g:plain

センサ無しでセンサと連携する

マウスを操作するプログラムが完成した頃に、距離センサ2つのはんだ付けが終わったらしく、距離センサが使えるようになりました。
しかし、残りのセンサはフェリーにあるらしくフェリーに戻るまで、これ以上はセンサは増えないとのことでした。

また、先程までは宿の2階で作業していたのですが、施設利用の都合上3階の部屋に移動することになります。

しかし、3階の部屋はテーブルと椅子のほとんどを他チームを使用しており、
開発環境がフローリングになってしまい、センサを使いづらい環境だった(地面だと踏む可能性が高い)ため、
1度だけセンサとの接続しシリアル値が取得できることを確認したあとはセンサなしで実装を行いました。

センサを使わないので擬似的なセンサを /dev/urandom を使ってつくりました。

mock_ttyACM.sh

#!/bin/bash
## /dev/ttyACM0 の値を適度に再現
cat <(cat /dev/urandom |
  LANG=C sed -re 's/[^0-9]//g' -e 's/0*//' -e 's/(....).*/\1/' -e 's/^$/0/' |
  xargs -L1 -i sh -c 'echo {};sleep 0.1')

このスクリプトと、ファイルディスクリプタを下記のように利用することによって

$ cat <(cat ./mock_ttyACM.sh)
$ cat /dev/fd/63

実際に下記のようなシリアル通信の値を読み取っている状態を擬似的に再現することができます。

$ cat /dev/ttyACM0

f:id:amanoese:20190209024212g:plain

疑似センサができたのでセンサをスクリーンの周りに設置し、
手などでセンサの範囲内に入ったときの位置を求めるコードを書きます。
本当はセンサを1列に並べるのが良いらしいのですが、私の頭が悪かったためメッシュ状に並べることを想定してしまいした。

この際に、 マルチタッチを考慮しない場合は各軸センサで最小な値が物体の座標の位置である
という特性に気づいたため、その特性を利用します。

N個のデバイスを引数に取り同期ぽいことしながら、座標を求めるスクリプトを書きます。

senser-joiner.sh

#!/bin/bash

temp=`mktemp`
device_num=6

for x in "$@"
do
  echo "$x" >> temp
done

cat temp |
  xargs -L1 -i sh -c 'cat {} | head -1' |
  sed -r -e 's/(....).*/\1/' -e 's/^$/0/' |
  xargs -n${device_num} |
  sed -r 's/^0+//' |awk '{min=10000;for(n=1;n<=NF;n+=1){if($n<min){min=$n}};print min}'

rm temp

疑似センサの値をスクリプトに渡すテストコードを書きます。

test-run.sh

#!/bin/bash
./senser-joiner.sh <(cat ./mock_ttyACM.sh) <(cat ./mock_ttyACM.sh) <(cat ./mock_ttyACM.sh) <(cat ./mock_ttyACM.sh) <(cat ./mock_ttyACM.sh) <(cat ./mock_ttyACM.sh)

実行します。

$ ./test-run.sh
2485
6713

1行目にX座標、2行目にY座標が表示されます。

X座標のセンサの数 <= Y座標のセンサの数
である限り問題のないコードです。
もしY座標のセンサが多い場合は入力を逆にし tac すれば良いです。

このプログラムはセンサの数が2つ以上であれば何個でも動作できます。
センサ4つだと検出範囲はこんな感じになります。

f:id:amanoese:20190209144013p:plain

センサの数に依存していないプログラムなので
理論上はセンサの数がが多ければ多いほど検出範囲が広がるはずです。

これでセンサからの座標取得のプログラムは雑に完成します。

センサ2つで

座標取得のプログラムできたあたりで船に戻ります。
また、出来上がったプログラムを実際のセンサ2つでテストしてみます。

実際のデバイスから座標をとる起動スクリプトを書きます。

get.sh

#!/bin/bash
echo /dev/ttyACM{1,0} | xargs -i sh -c 'sleep 0.01;sudo ./senser-joiner.sh {} | xargs'

また無限ループで一定時間ごとに座標を取得し、
入力があった際に画面上の座標位置に移動しクリックするスクリプトを書きます。

run.sh

yes |
  xargs sh -c 'sleep 0.1; ./get_x_y |
  awk "\$1<200&&\$2<300" |
  sed "s/ /\//" |
  sed "s@.*@http://localhost:4000/&@" | xargs -L1 curl'

実際のセンサの距離と画面の位置が合うように修正します。

index.js

// Move the mouse across the screen as a sine wave.
const express = require("express");
const app = express()
const robot = require("robotjs");

app.get('/:x/:y', (req, res) => {
  let {x,y} = req.params
  if(x == null || y == null) {return}

  let zoom = 1080 / 200  // ディスプレイサイズ (px) / 実査のセンサの値 (mm)
  let zoom_x = x * zoom
  let zoom_y = y * zoom

  robot.moveMouse(zoom_x,zoom_y);
  robot.mouseClick();
  console.log('click',{x,y})
  res.send(`click x:${x} y:${y}\n`)
})

app.listen(4000, () => console.log('Example app listening on port 4000!'))

この段階でUnityのプログラムがなかったので、Vue.jsで作ったアプリでテストします。

計算が雑で酷いですがセンサ2個でも位置が取れているぽく動いてます。

割り箸による高度な実装

本番ではプロジェクタで移すので、プロジェクタの周りに設置するための枠を作ります。
フェリーには高度な材料がなかったため割り箸で枠を作ります。

割り箸は横にすると断面2次モーメントが小さくたわんでしまうため、
縦を利用するほうが良いとの指摘をチームで唯一建築士資格を持つエンジニアから指摘を受けます。
角の部分を三角形にすることで強度を高めるアドバイスも受けます。

f:id:amanoese:20190209030329p:plain

材料が乏しい中ですが、割り箸、ガムテーム、養生テープで枠が完成します。

センサ4つで

この時点でセンサが6つ完成します。
しかし、USBハブを使ってもUSBポートが4つしかなかったため仕方なくセンサ4つを枠に取り付けて組み立てていきます。

組み立て後、枠の耐久性が低いことを考慮し、一旦地面において動作テストをすることにしました。
しかし、センサは赤外線を使用しており平面に近いとその平面を検知してしまうため若干地面と離す必要があります。

f:id:amanoese:20190209031510p:plain

仕方ないのでカップラーメンを土台にして地面から離してテストしていました。

疲れた自分と完成した枠。そして明日の予定を話し合うチームメンバーの写真があるのですが……
f:id:amanoese:20190209032435j:plain

その下で、カップラーメン「ごつもり」に支えられているセンサとその枠を確認できます。 f:id:amanoese:20190209032500j:plain

実際のセンサ数やスクリーンとの距離に合わせてプログラムを修正します。
そしてVue.jsで作ったアプリでテストします。

また作業中にセンサがちょくちょく死ぬことがわかったので、
センサの生存を確認するスクリプトを書いたりしてました。

chack.sh

#!/bin/bash
ls /dev/ttyACM* | xargs -L1 -i sh -c 'echo {};head -1 {}'

実行すると、センサが死んでいるのが容易に確認できます。

$ ./check.sh
/dev/ttyACM0 1235
/dev/ttyACM1 0 TIMEOUT
/dev/ttyACM2 1235
/dev/ttyACM3 1432

これがあるおかげで本番は楽にできました。

この段階で、Unityのプログラムはほぼ完成しますが Build で少しハマったらしく、
本番当日にbuildしたプログラムと合わせる段取りになります。

……正直ぶっつけ本番感でやばい。

3日目

本番ではプロジェクターの周りにセンサを配置するため枠を立てないといけないのですが、
枠の強度や、支えるものがなく枠の設置に悪銭苦闘します。

f:id:amanoese:20190209032802j:plain

絶望的な状況で奇跡的に運営の方から、昇ポールを提供されます。

これによりプロジェクターの近くに設置することに成功します。

f:id:amanoese:20190209033134j:plain

このあとUnityのプログラムもbuildに成功し、
特に問題も起きず動作しました。

結果

リーダーの素晴らしい統率力、Unityのプログラム。
また素晴らしいスライドのデザインや構成、多くのハードウェアデバイスを扱えるエンジニア、それをサポートするエンジニアがいたこと。
運も味方したこともあってか、最優秀をいただくことができました。

個人的にはセンサの値をもとに良い感じにクリックするプログラムを開発していただけなので、優秀なチームメンバーに感謝です。

感想

割り箸から昇ポールの流れは本当に運が良かったです。

あとテストツールをたくさん作ってあったおかげか、本番での問題はほぼなかったのが印象的でした。
TDDは素晴らしいですね。TDDよく理解してないですけど。

ネットが使えない環境での開発辛い!
まあ、だからといってネットがなくても開発するのが真のエンジニアだと思います……嘘です。ネット無いと死にます。ネットください。

こういう短期決戦の開発では、シェル芸の知識がかなり活きますね。
あとShell Sctiptのほとんどはシェル芸で書いたあとにファイルに貼り付けてるだけなので、コードは汚いですがかなり早く開発できました。
SDD(ShellGey-Driven Development)として提唱していきたい…ユニケージ開発手法?知らない子ですね。

というより「自称うぇっぶふるすっくえんじにあ」名乗って参加しているかつ一応Webなサービス作るの得意なフレンズなんですが、Web要素一切無い開発だったんですよね……
最初のVue.jsのアプリくらいじゃ…しかしあれ最終的にはElectronにする予定だったし……ハードウェアは趣味でやってたから理解できたけど……

あと次参加するときまでにUnity覚えていきたい。

あと最優秀賞だったのでフェリーのペアチケットもらえたのですが誰と行けば良いんでしょうか?