スーパーお手軽ハードウェアをMicroPythonでウェイする

こんばんは。これは #kosen10s Advent Calendar 最終日の記事です。 昨日は @e10dokupノリと勢いで買ってよかったものベスト10 2017 でした。

e10dokup.hateblo.jp

MicroPythonはすばらしい

この記事はMicroPythonの素晴らしさをノリで世の中に伝えるものであり、勢いで書かれていることをどうか許してほしい。

筆者の紆余曲折のせいでなんだかんだArduino SDKの話が長いが、そこは飛ばしていただいても結構である。

弊社におけるチャイム文化の隆盛と没落

時は2015年・年末のオフィスにて、コアタイムの開始と終了をチャイムで知らせる文化が生まれた。もともとは開始の朝10時に簡易アラームを鳴らしていたのだが、時間が正確でない(ほうっておくと平気で30秒遅れる)ことから手で鳴らそうという話になったのが元である。

f:id:puhitaku:20171225173449j:plain

だがそれをやるのは人間。数日はワイワイ言いながらチャイムを鳴らしているが、7日も経てばもう眉もピクリとすら動かさなくなる。結局終了時間の15時はおろか、朝の開始チャイムすら鳴らし忘れて元のアラームに頼る事態になってしまった。

「これではいかん…」と社内に重苦しい雰囲気が…特に流れなかった。

社内チャイムの建立

まあでも実際よくないので、当時の筆者はこの事態をどうにかするため大仏を建立自作ハードでチャイムを鳴らすように対処することにした。その時採用したのは以下の構成である。

  • バーチャイム
  • ESP8266
  • ESP8266 Arduino SDK
  • サーボ2つ(音色を選ぶサーボと叩くサーボ)

そう、理由は明白。今までATMEGA328と共に培ったArduinoの知識がそのまま使えるからである。だがこの選択は結果として本末転倒の面倒くささにつながってしまった。

(参考)esp8266/Arduinoの実装のゆらぎ

この節はただの悲しい歴史なので次節まで飛ばしてもらってもOK。

github.com

Arduinoはg++のオプションを変えたらC++14も利用できるので、当時の筆者は vectorautoconstexpr にまみれるにわかC++に興じようとして失敗した。

結論から言うと、当時の esp8266/Arduino に施された「Arduino SDKの min()max() をグローバルに実装するFixを入れた結果STLが全部死ぬ」というトンデモFixに足を取られ、std::vector を使う即席パッチを当てるのに数時間戦った。以下解説である。

min()max() は、Arduino SDKで一般に提供されるごく基本的な関数である。これら2つは太古の昔からぶっちゃけdirtyなマクロで実装されていて(例: ATMEGA)、当時の esp8266/Arduino のリリース2.0.0でもATMEGAなどから移植する形で同じく存在していた

これに関連するIssueが以下である。

github.com

あるユーザーが「min()とmax()がないって怒られるんだけど」と報告したところ、Maintainerはminとmaxの定義を移動した#ifdef __cplusplus の下に場所を変えることで対処したようだが、筆者ですらこの変更は危険に見えるし、少し失礼になってしまうがMaintainerにはもうちょっとゆっくり考えてほしかった感がある。

パッチの結果として、問題をFixするどころか元々 std::minstd::max を使っている std::vector がマクロに足を取られてビルドできなくなる状態に陥っていた。そもそもATMEGAのArduino SDK(本流)ではSTLをサポートしていない(らしい)ため std::minstd::max がなくても問題なかったのだが、ここへ来て名前が衝突してしまい、本体にすでにあったライブラリである ESP8266WiFiMulti.h でもvectorを使っていたりしてどうするか迷う事態になってしまった。

ちなみにその後2016年3月になってCollaboratorが「そのdefineはC++の機能(vector)をぶっ壊すで」と発言し、その2ヶ月後に minmaxalgorithm が提供する正真正銘の std::minstd::max を使うように改められた。加えてかつてのマクロによる実装も _min()_max() という名前で残され、必要なときはこのworkaroundで対処してくれという事になった。この一連の流れは2016年5月〜6月ごろだが、masterにmergeされたのはつい先月である。だから最新のTagである 2.4.0-rc2 にも含まれていない。惨禍は長いこと続いていたようだ。

ちなみに当該パッチでビルドが通らなくなった ESP8266WiFiMulti.h (vectorを使っていた)は、例の minmaxundef するというなんとも対症療法的に解決していて、ここはまだ undef が書かれっぱなしである。まあビルドは通るのだろう。

なんとか作りきった

Arduino IDE特有の処理やビルドエラーに悩まされながらもC++らしい形でなんとか書ききった。半日ごとにNTPとsyncしつつ、コアタイムの開始と終了に合わせて鳴らすことに成功した。

こうして完成したのが初代チャイムである。

github.com

(以前の実装なので v1 ブランチを切っている)

f:id:puhitaku:20160406095901j:plain

社内プレゼン大会での発表を急いだこともあり、サーボ直付け x2の土台をユニバーサル基板で作るという雑さとなってしまったが、思いのほかこれが堅牢で偶発的な不具合(多分NTP)を年に2回ほど踏むほかは完璧に動作した。叩く誤差もだいたい±1秒ぐらい。

社内チャイムの滅亡

しかし幸せは1年半ほどで尽きてしまう。チャイムからのびるUSBケーブルに同僚が足を引っ掛けて落下、チャイムを叩くサーボが死んでしまったのだ。筆者が雑に配置したが故の災害だった。

サーボを分解すると、ギアが砕け散っていた。写真を見ると、右上のギアが一部だけ残り傾いているのがわかる。

f:id:puhitaku:20171222172807j:plain

今度はMicroPythonで実装してみた

さて、もう一度C++をビルドするにしても、即席パッチやSDKバージョン依存により秘伝のタレみたいだったビルド環境はVAIO Pro 11の売却により失われていた。 そこで、今回はMicroPythonで書いてみることにした。

github.com

MicroPythonはマイコンなどMMCを持たない環境をメインターゲットとしたPython 3の実装である。もちろんPOSIXの上に乗っかった unix ポートもあるためLinuxやmacOSで動作させることもできる。PyCon JP 2016のLT枠にて、拙作「走るルータ」のPython 3 + FlaskだったAPIをMicroPython + プレーンなTCPパケット処理に置き換える内容のLTもしたが、それ以外では久しぶりな感じである。

www.zopfco.de

speakerdeck.com

ESP8266へのインストール

インストールはかんたん(マニュアルはこちら)。FTDIの変換チップ等を介してESP8266のシリアルとおしゃべりできるようにして、 esptool.py 経由でeraseとflashを実行するだけだ。

ファームウェアはこちらからダウンロードできる。日本で最も普及しているであろう ESP-WROOM-02 であれば、一番上の最新のStableリリースをダウンロードすればOK。

$ pip3 install esptool

# macOSなら/dev/cu.なんとかかんとかになるはず
$ esptool.py --port /dev/ttyUSB0 erase_flash
$ esptool.py --port /dev/ttyUSB0 --baud 115200 write_flash --flash_size=detect 0 esp8266-20171101-v1.9.3.bin

公式ではボーレートを 460800 にしているがこれは高速すぎて変換チップや物理的条件によっては失敗する。実際筆者もハンドシェイクに失敗したため、 115200 に落として実行している。

ここでGPIO 0のプルダウンを解除して再起動しEnterを押すと、おなじみの >>> プロンプトが現れる。

>>> import sys
>>> print(sys.implementation)
(name='micropython', version=(1, 9, 3))
>>> print(sys.platform)
esp8266

すっごーい!

Wi-Fiに秒でつなげる

>>> import network
>>> sta = network.WLAN(network.STA_IF)
>>> sta.connect('ser_kaba', 'toki_paca')
>>> sta.isconnected()
True
>>> print('Addr={}, Mask={}, GW={}, DNS={}'.format(*sta.ifconfig()))
Addr=172.16.3.76, Mask=255.255.252.0, GW=172.16.1.1, DNS=172.16.1.1

ビーバー: それ(REPL)いいっすね。 これで(カタカタ)どうっすか。

サーバル: はやっ!

はいたったこれだけ!!!

SSIDは嘘っぱち。ser_kaba がESSID、 toki_paca がPSKの位置となる。 本来なら sta.isconnected()while で待つ以外に特に注意点はない。

NTPと秒でおしゃべりする

>>> import time
>>> import ntptime
>>> ntptime.host = 'ntp.nict.jp'
>>> time.localtime(ntptime.time())
(2017, 12, 25, 14, 18, 36, 0, 359)

プレーリー: なるほど、こういうこと(カタカタ)でありますな。

サーバル: はやっ!

はいたったこれだけ!!!

Wi-Fiにつながっている前提ではあるが、マジでこれだけで時間が取ってこれる。控えめに言って神。

ちなみにタイムゾーンを扱う実装はMicroPythonの標準ライブラリにはまだないため、timeはすべてUTC時間となる。 とはいえ ntptime.time() に時差分の秒数を足せば良いので特に問題はない。

サーボを秒で動かす

>>> from machine import Pin, PWM
>>> p = PWM(Pin(13), freq=200)
>>> p.duty(400)
>>> p.deinit()

ジャガー: MicroPythonでESP8266を駆動すればいいんだね?

サーバル: ありがとう助かったよ。ビルド環境がなくなって、困ってたんだ。

ジャガー: このあたり、libcの実装ごと変わっちゃうからね。

はいたったこれだけ!!!

名前付き引数で渡せるデューティ比が感動モノだ。

できたよ

f:id:puhitaku:20171225235202j:plain

f:id:puhitaku:20171225235316j:plain

github.com

上に書いてあるようなコードをいい感じにウェイした新生チャイムが…業務時間に作ってるから多分26日の日中には完成するであろう! とりあえずハードに関してはサーボを交換し完成したが、時間の管理部分がまだなので随時Pushしていく。

ちなみに本記事に登場するけものフレンズのセリフは以下からお借りした。最高のリポジトリに感謝。

github.com