2018年09月18日

ISUCON 8 予選で惨敗しました(リュウグウ)

はてなブックマークに登録

@methane です。とうとうISUCON予選敗退を経験してしまいました。 めちゃんこ悔しいです。

16:20には6万点台を出し、そこからはトップ争いを続けて17:47には10万点台を叩き出したものの、その後は2万点台しか出なくなり終了してしまいました。

それまでは調子の悪いときでも数回ベンチをしていれば少なくとも4万点台にはなっていたので、ずっと2万点台しか出なかったのは不運もあったのですが、そもそもスコアが安定しない原因を潰しておけば確実に予選突破できていたはずなのでこれも実力です。

やったこと

役割分担は、僕が全体を見る&アプリの実装もやる、 makki_d がアプリ、 mapk0y がインフラでした。選択言語はGoです。

最初の方はダッシュボードを作るのに必要な作業を2人にお願いしつつ、自分でもインフラ、アプリのコード両方を見て回りました。

ダッシュボードができてからは getEvent がネックなのが明らかになりました。一方レギュレーションでは予約とキャンセルのスコアが高く、ISUCON2との類似性からもその部分もキーになると判断しました。

getEvent も 予約もキャンセルも、全部 registration テーブルがキーになっています。とくに予約処理は見るからにまずいトランザクションになっているので、 registration テーブルのオンメモリ化に取り掛かりました。

といっても完全なオンメモリ化は諦めていて、ちゃんとDBに書き込んだ上で最新の情報をメモリ上にも載せておくだけです。また、メモリに乗せるのはキャンセルされていない予約だけです。

最初は予約処理とキャンセル処理でメモリ上のデータを正確に更新するところを作りきりました。これができたのがすでに15時ごろでした。(diff)

これができると、次は getEvent でループで registration テーブルを SELECT してるところをメモリ上のデータを使うように書き換えます。 (diff) これが 15:30 頃で、4万点台でやっとトップグループの仲間入りをし、ほっと一息。

次に、さっきの getEvent のループ内で RegisteredAt を求めるためだけに registration を配列から線形探索でやっていたのがネックになっていたので、ループの前に先に sheet id と registered at を対応付ける map を作って置くことで高速化 (16:10ごろ) (diff)

この時点で6万点台は出るようになっていたものの、スコアの乱高下に翻弄され始めます。インフラの設定変更を試してみても大幅にスコアが下がって、設定変更のせいなのかどうか分からなくて迷走してしまいます。 (TIMED_WAIT が多いから nginx -> Go を unix socket に切り替えたものの、スコアが上がらなくなって TCP に戻すなど)

17:00 を過ぎた当たりで、 makki_d が挑戦していたイベント全体のオンメモリ化のバグが取り切れなかったので、 getEvents で event テーブル全体を引いたあとに getEvent で event テーブルを1行ずつ引くという同じテーブルに対する N+1 を潰し (diff)、17:30 には7万7千点。

そこから再起動試験で一度失敗し、2度めの再起動試験で幻の10万点台を 17:47 に出します。

ここで止めておけば良いものの、この一度のハイスコアで「再起動したらスコアがよくなる、なんか遅くなるのは繰り返して実行してるからかもしれない」と誤認してしまいます。 このときに nginx のアクセスログが切れてなかったので、アクセスログを切ってもう一度やろうと指示を出し、結果競技終了までずっと2万点台、最後の1回は1万点台で終了しました。

敗因(技術的問題点)

終了後にメンバーと話し合って、多分これだという問題点を見つけました。

予約とキャンセル処理では、registration のオンメモリキャッシュの一貫性を保護する mutex の中でDBへの更新をしていました。それがつまり気味なのはなんとなく把握していましたが、原因や、ひどいときにどれくらい詰まるのかまでは把握してませんでした。

一方、DBの方でもなにかデッドロックのログが発生していることは気づいていたものの、 admin 系で複雑なクエリがあるので、 admin 系のAPI通しで競合する事があるのかなとあまり気にしていませんでした。

そして、その admin 系の複雑な長いSELECTクエリに、 "for update" が混ざっているものがあることも気づいていて無視していました。

これを総合して考えると、 admin 系の重い SELECT クエリが for update で registration テーブルを盛大に(全体を?)ロックし、予約か削除がロックを握ったままそれを待ち、 getEvent を利用しているすべてのAPIが引きずられて詰まりタイムアウトを発生させる、というシナリオに思い当たります。ベンチマーカーが公開されたら検証してみたいと思います。

実は、mapk0y が作業中に「MariaDBからMySQLに入れ替えます。」「トランザクション分離レベルを read committed から repeatable readに切り替えます」という報告はしてくれていました。SELECTクエリの中で for update が出てくるの、ダーティーリードを避けるためにロックを掛けていたとすれば、 repeatable read に切り替えたときに完全に不要になっていたはず…ぐぬぬ。

訂正: read committed になっていたのは mapk0y が MySQL に入れ替えるときに my.cnf に誤混入しただけで、初期実装は repeatable read だったようです。あのJOINを使ったずっと横スクロールしないと読めない長い一行SELECT文の中に無意味な for update が混じっていたのは、特に必要な理由がない純粋な罠だったのかもしれません。。。

反省点

最大の書換であるところの reservation オンメモリ化を自分でしてしまいましたが、これは makki_d に任せて、自分はもっと調査系に専念するべきでした。手を付けたときに makki_d が他の作業に取り掛かっていたのと、事前練習不足で makki_d が「オンメモリ化」をスラスラ書けるか解らなかったのと、何より「自分が何もできないまま敗北する恐怖」に負けてしまいました。

大きな改修+それを利用した大規模な高速化が終わってトップグループになったのが16時で、そこからは「競技終了までに何をするか/しないか」に頭が切り替わってしまったので、落ち着いて全体を注意深く調査することができませんでした。

他にも、次のような要因があったと思います。

  • 過去問では report 系APIは整合性検証のためだけに使われることが多かったのでほとんど見てなかった
  • 通過ラインがわからない恐怖から、遅くなる原因追求よりも調子のいいときのスコアアップを優先してしまった

本戦では1位を取るためにスコアアップ優先になるのはある程度しかたありませんが、予選ではトップスコアを取っても通過できなければ意味がありません。来年はもう少し落ち着いて、仕事で負荷対策の調査をするときのようにマズイ点を潰していきたいと思います。

感想

アプリのコードの規模、DBのスキーマの規模ともにISUCONに適したボリュームに抑えつつも、完全オンメモリ化(SELECTを削除し、DBへの書き込みをロック外に追い出しても整合性が担保できる)が難しい複雑なSQLが散りばめられているバランスが素晴らしかったです。いろんなチームがそれぞれの得意なやり方で勝ちを狙えたはずで、本戦にしないのがもったいない、これ以上ないくらいの良問でした。

また、去年の予選でやらかしてしまった原因の「予選で複数台構成」を今年も継承してきたことにも驚きました。参加者視点で見たら、万が一設定をミスってSSHできなくなるような事態になっても試合終了にならない安心感は良いですね。

その複数台構成の活用方法も、去年に比べてずっと自由度が高いのが良かったです。

本当にこれまでで最高のISUCONだったと思います。ありがとうございました。 これが予選なら本戦はどうなるのか、参加できないのが悔しくて仕方ありません。

songofacandy at 11:04
この記事のURLComments(0)ISUCON 
2018年09月05日

ESP32 モジュールのフラッシュメモリ暗号化機構に関するメモ

はてなブックマークに登録

ESP-WROOM-32 の内蔵フラッシュメモリについて調べていたところで Flash Encryption に興味を持ちその便利さを実感しました。 利用方法にやや込み入った要素も含まれるため自分用のメモとして一連の情報を控えておくことにします。

重要: この記事には誤りが含まれている可能性があります。たとえその誤りが原因で何処かで何らかの損害が発生したとしても筆者ならびに当社は一切の責任を負いません。実験や操作は上記公式ドキュメントを熟読の上必ず自己責任で慎重に行って下さい。

概要

ESP32 モジュールのフラッシュメモリは esptool を使えば簡単に読み書きできるためバックアップ等に便利ですが、その一方で、自分の手を離れた場所で装置を稼働させる場合などに他者に不正なクローンを作成されたりコードに含まれるセンシティブな情報を読まれてしまう可能性もあります。 こういった懸念は Flash Encryption 機構を利用すれば格段に軽減されます。所定のモジュールのもとで暗号化したイメージを他のモジュールへ持ち込んで稼働させることはできず、そこから情報を窃視することもできません。

ただし、Flash Encryption の利用には大きくふたつの注意すべき点があります。

  1. ESP32 チップ内の OTP メモリへの不可逆な書き込みを伴う
  2. ひとたび Flash Encryption を適用したモジュールにおいてそれ以降のイメージの更新時に再暗号化を実施可能な回数は「3 回」まで

このように、アクティブな開発や実験のフェーズ向きではなくおおむねプログラム完成段階での本番機や製品への適用を想定した機構と言えるでしょう。そういった事情も相まってか、これを積極的に利用している個人ユーザは今のところあまり多くないように見受けられます。

フラッシュメモリ暗号化の多くのメリットを考えると惜しく感じられますが、実際には上記 2. の制限は「事前生成暗号化キー」を適切に取り回すことで回避が可能です。その方法であれば Flash Encryption をごく手軽に扱えるため、たとえばプロトタイピング段階での試作機での利用など様々な活用方法が考えられるでしょう。

以下の記事では、最初に Flash Encryption のしくみの整理を行い、次に上記の事前生成暗号化キーを利用する手順と実際にその操作を行った記録を列挙、最後に Arduino core for ESP32 環境での対応方法等について手元で行った実験の内容とその結果に触れています。 なお、Espressif は別のセキュリティ機構である Secure Boot との併用を推奨していますが、この記事ではまず Flash Encryption 単体に注目しています。

  • 手元では Linux 環境で作業を行っており、記事中のコマンド発行例などに「/dev/ttyUSB0」等の環境に即した記述が含まれます
  • Flash Encryption の操作で使用する esptool, espefuse, espsecure の各ユーティリティは、Linux, MacOSX 環境用には Python スクリプトとして、Windows 環境用には exe 実行形式で提供されています。記事での構文例には上と同じ理由で ".py" の拡張子を記述している箇所があります

Flash Encryption のしくみ

まず、公式ドキュメントの内容をもとに Flash Encryption のしくみを整理します。

全体像

  

  • Flash Encryption は ESP32 モジュールの内蔵フラッシュメモリ上の所定のパーティションの内容を AES-256 で暗号化するもの
  • 暗号化キーは当該モジュールで初めて暗号化を行う際に ESP32 チップ内の efuse 領域内にランダムに生成され永続的に保持される。生成されたキーへの外部からのアクセスはデフォルトで不可
    • ESP32 efuse の詳細 => espefuse - github.com/espressif
  • 暗号化はブートローダが行う。具体的には、make menuconfig - [Security Features] - [Enable flash encryption boot] オプションを有効にした状態でビルドしたブートローダが、他のパーティションイメージとともに make flash された後の初回起動時に一括して暗号化を実施する
    • つまり、上記オプションはその目的に即したブートローダコードを生成するものであり、その他のパーティションイメージは通常の内容でビルドされる
  • 暗号化の対象は以下のとおり
    • ブートローダ (オフセット 0x1000, 最長サイズ 0x7000 バイト
    • パーティションテーブル (オフセット 0x8000, サイズ 0xC00 バイト
    • Type = app のパーティション
    • encrypted フラグの指定されたパーティション
      • ただし、手元の確認では設定上 spiffs パーティションの定義に「spiffs,data,spiffs,0x291000,0x16F000,encrypted」の要領で指定を付与し暗号化を有効にしたところプログラム実行時に次のエラーが発生した: "spiffs can not run on encrypted partition"
    • nvs パーティション (key-value ストア領域) を暗号化することはできない。 spiffs と同様に nvs にも専用の API セットが用意されておりそっち側の実装とのかねあいだったり?
    • ドキュメント上の関連箇所の引用。ちなみに手元ではまだ使ったことのない「phy」パーティションについての言及があるが、「physical access readout」が NG につき esptool 等で読み出し不能であるためトレードオフを理解した上で利用すべきだろう
      • None of the default partition tables include any encrypted data partitions.
      • It is not necessary to mark “app” partitions as encrypted, they are always treated as encrypted.
      • The “encrypted” flag does nothing if flash encryption is not enabled.
      • It is possible to mark the optional phy partition with phy_init data as encrypted, if you wish to protect this data from physical access readout or modification.
      • It is not possible to mark the nvs partition as encrypted.
  • 稼働中のプログラムからフラッシュメモリ上のデータ参照の際の復号は透過的に行われる

注意点

Flash Encryption においては 8 ビットの「FLASH_CRYPT_CNT」efuse が非常に重要。あらかじめその役割と意味を正しく把握しておく必要がある。

  • まず、手元にある Flash Encryption 未実施の ESP32-DevKitC に対し Espressif 提供の espefuse ユーティリティの summary コマンド, dump コマンドを実行した結果を以下に示す
    $ espefuse.py --port /dev/ttyUSB0 summary
    espefuse.py v2.3.1
    Connecting.....
    Security fuses:
    FLASH_CRYPT_CNT        Flash encryption mode counter                     = 0 R/W (0x0)
    FLASH_CRYPT_CONFIG     Flash encryption config (key tweak bits)          = 0 R/W (0x0)
    CONSOLE_DEBUG_DISABLE  Disable ROM BASIC interpreter fallback            = 0 R/W (0x0)
    ABS_DONE_0             secure boot enabled for bootloader                = 0 R/W (0x0)
    ABS_DONE_1             secure boot abstract 1 locked                     = 0 R/W (0x0)
    JTAG_DISABLE           Disable JTAG                                      = 0 R/W (0x0)
    DISABLE_DL_ENCRYPT     Disable flash encryption in UART bootloader       = 0 R/W (0x0)
    DISABLE_DL_DECRYPT     Disable flash decryption in UART bootloader       = 0 R/W (0x0)
    DISABLE_DL_CACHE       Disable flash cache in UART bootloader            = 0 R/W (0x0)
    BLK1                   Flash encryption key                              
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
    BLK2                   Secure boot key                                   
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
    BLK3                   Variable Block 3                                  
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
    
    Efuse fuses:
    WR_DIS                 Efuse write disable mask                          = 0 R/W (0x0)
    RD_DIS                 Efuse read disablemask                            = 0 R/W (0x0)
    CODING_SCHEME          Efuse variable block length scheme                = 0 R/W (0x0)
    KEY_STATUS             Usage of efuse block 3 (reserved)                 = 0 R/W (0x0)
    
    Config fuses:
    XPD_SDIO_FORCE         Ignore MTDI pin (GPIO12) for VDD_SDIO on reset    = 0 R/W (0x0)
    XPD_SDIO_REG           If XPD_SDIO_FORCE, enable VDD_SDIO reg on reset   = 0 R/W (0x0)
    XPD_SDIO_TIEH          If XPD_SDIO_FORCE & XPD_SDIO_REG, 1=3.3V 0=1.8V   = 0 R/W (0x0)
    SPI_PAD_CONFIG_CLK     Override SD_CLK pad (GPIO6/SPICLK)                = 0 R/W (0x0)
    SPI_PAD_CONFIG_Q       Override SD_DATA_0 pad (GPIO7/SPIQ)               = 0 R/W (0x0)
    SPI_PAD_CONFIG_D       Override SD_DATA_1 pad (GPIO8/SPID)               = 0 R/W (0x0)
    SPI_PAD_CONFIG_HD      Override SD_DATA_2 pad (GPIO9/SPIHD)              = 0 R/W (0x0)
    SPI_PAD_CONFIG_CS0     Override SD_CMD pad (GPIO11/SPICS0)               = 0 R/W (0x0)
    DISABLE_SDIO_HOST      Disable SDIO host                                 = 0 R/W (0x0)
    
    Identity fuses:
    MAC                    MAC Address                                       
      = 30:ae:a4:02:59:c0 (CRC 9b OK) R/W 
    CHIP_VER_REV1          Silicon Revision 1                                = 0 R/W (0x0)
    CHIP_VERSION           Reserved for future chip versions                 = 0 R/W (0x0)
    CHIP_PACKAGE           Chip package identifier                           = 0 R/W (0x0)
    
    Calibration fuses:
    BLK3_PART_RESERVE      BLOCK3 partially served for ADC calibration data  = 0 R/W (0x0)
    ADC_VREF               Voltage reference calibration                     = 1100 R/W (0x0)
    
    Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V).
    
    $ espefuse.py --port /dev/ttyUSB0 dump
    espefuse.py v2.3.1
    Connecting....
    EFUSE block 0:
    00000000 a40259c0 009b30ae 00000000 00000036 00000000 00000000
    EFUSE block 1:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    EFUSE block 2:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    EFUSE block 3:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    
  • FLASH_CRYPT_CNT は暗号化を行った回数と暗号化の有効無効状態を管理する。初期値は上のとおりゼロ (BIN: 0000 0000)であり、ブートローダが初めて暗号化を行った時点で BIN:0000 0001 に変化する (不可逆)
  • FLASH_CRYPT_CNT の ON ビット数が偶数の場合は暗号化が無効な状態、奇数の場合は暗号化が有効な状態であることを示す
  • FLASH_CRYPT_CNT の ON ビット数が奇数、すなわち暗号化が有効な状態で別のプレーンなイメージを make flash すると起動不可となる。これは、ESP32 が efuse 領域に保持しているキーで暗号化されたイメージの期待される状況であるにもかかわらず場違いなイメージに直面した結果の所作
  • 上記のような場合には以下の手順で Flash Encryption を無効化することができる (注:手元ではこの操作は未確認)
    1. make menuconfig - [Security Features] - [Enable flash encryption boot] オプションを確実に無効化して make && make flash
    2. 手作業で FLASH_CRYPT_CNT の ON ビット数を偶数にすることで暗号化無効の状態にする。具体的には、espefuse ユーティリティを使って FLASH_CRYPT_CNT に対し burn_efuse コマンドを発行すると現在もっとも上位の ON ビットの左側のビットが ON になる。つまり、FLASH_CRYPT_CNT = BIN:0000 0001 の状態で実行すると BIN:0000 0011 に変化する => ON ビット数が偶数なので暗号化無効状態
      espefuse.py --port /dev/ttyUSB0 burn_efuse FLASH_CRYPT_CNT
    3. ESP32 ボードをリセットすると正常に稼働する

  • OTP メモリである efuse 領域への変更は不可逆であるため、8 ビットの FLASH_CRYPT_CNT に対する上記の「暗号化と無効化」の上限回数は 4 回。以下に状態の一覧を示す。 9. の無効化 4 回めで全ビットが ON = 0xFF になると当該 ESP32 モジュールでの Flash Encryption は永久に無効となる
    1. BIN:0000 0000 工場出荷時
    2. BIN:0000 0001 暗号化 1 回め
    3. BIN:0000 0011 無効化 1 回め
    4. BIN:0000 0111 暗号化 2 回め
    5. BIN:0000 1111 無効化 2 回め
    6. BIN:0001 1111 暗号化 3 回め
    7. BIN:0011 1111 無効化 3 回め
    8. BIN:0111 1111 暗号化 4 回め
    9. BIN:1111 1111 無効化 4 回め

  • なお、ある時点での FLASH_CRYPT_CNT の状態を永続化させたい場合は espefuse.py の write_protect_efuse コマンドを実行する。誤って実行すると手詰まりになるため要注意
    espefuse.py --port /dev/ttyUSB0 write_protect_efuse FLASH_CRYPT_CNT
  • また、以下の事情にも注意が必要
    • Read- and Write- protecting efuses - espefuse - github.com/espressif
      NOTE that efuses are often read/write protected as a group, so protecting one will cause some related efuses to become protected. espefuse.py will confirm the full list of efuses that will become protected.

事前生成暗号化キーの利用による Flash Encryption

ESP32 チップ内部に保持されるフラッシュメモリ暗号化キーはホスト PC 上で生成することが可能です。キーの生成を ESP32 側で行うとそれ以降の暗号化処理は前述の FLASH_CRYPT_CNT の管理下でブートローダへ委ねることになりますが、ホスト PC 上でキーを生成すれば開発者が主体的に所定のイメージの暗号化を行うことが可能となり、貴重な FLASH_CRYPT_CNT を消費することなく Flash Encryption を継続的に利用できます。ホスト PC 上で生成したキーは espefuse ユーティリティの burn_key コマンドで ESP32 の efuse 領域へ書き込んだ上で使用します。

概要

ドキュメントより。

  • Reflashing via Pregenerated Flash Encryption Key - Flash Encryption - esp-idf.readthedocs.io
    Reflashing via Pregenerated Flash Encryption Key

    It is possible to pregenerate a flash encryption key on the host computer and burn it into the ESP32’s efuse key block. This allows data to be pre-encrypted on the host and flashed to the ESP32 without needing a plaintext flash update.

    This is useful for development, because it removes the 4 time reflashing limit. It also allows reflashing the app with secure boot enabled, because the bootloader doesn’t need to be reflashed each time.

    Important

    This method is intended to assist with development only, not for production devices. If pre-generating flash encryption for production, ensure the keys are generated from a high quality random number source and do not share the same flash encryption key across multiple devices.
           :
    Google 訳:
    事前生成されたフラッシュ暗号化キーによるリフラッシュ

    ホストコンピュータにフラッシュ暗号化キーを事前に生成し、ESP32のefuseキーブロックに書き込むことができます。これにより、データをホスト上で事前暗号化し、平文フラッシュ更新を必要とせずにESP32にフラッシュすることができます。

    これは4回のリフラッシュ制限を取り除くため、開発に役立ちます。また、毎回ブートローダをリフラッシュする必要がないため、セキュアブートを有効にしてアプリをリフラッシュすることもできます。

    重要

    このメソッドは、開発の支援のみ目的としており、本番用のデバイス用ではありません。プロダクション用のフラッシュ暗号化を事前生成する場合は、キーが高品質の乱数ソースから生成され、複数のデバイス間で同じフラッシュ暗号化キーを共有しないようにしてください。
           :
多くの個人ユーザの作業の大部分はまさに「開発」であり、その過程でフラッシュメモリ暗号化の恩恵を享受できることは有り難い。事前生成暗号化キーを利用した Flash Encryption の流れを下図に示す。

手順

以下の一連の手順では、既出の esptool, espefuse に加え、espsecure ユーティリティを使用する。

初回の暗号化を終えるまで

第一段階として、まず初回の暗号化までを完了させる。ホスト PC 上で生成した暗号化キーを ESP32 の efuse へ先行して書き込むことがここでのポイント。

  1. 暗号化キーをホスト PC 上で生成する。初回のみ。絶対に紛失しないこと
    espsecure.py generate_flash_encryption_key ./mykey.bin
  2. 生成したキーを ESP32 の efuse へ書き込む。取り消し不可
    espefuse.py --port /dev/ttyUSB0 burn_key flash_encryption ./mykey.bin
    ※この手順で下記「3. 初回の Flash Encryption」に先行して efuse へ暗号化キーを書き込むことにより、本来はブートローダによる初回の暗号化実行の前処理として ESP32 チップ内で行われる暗号化キーの生成がスキップされる
  3. 初回の Flash Encryption を実行
    初回の暗号化は事前生成キーを使わない場合の手順と同じ
    1. make menuconfig で [Security Features] - [Enable flash encryption boot] オプションを有効にして保存終了
    2. make を実行
    3. make flash monitor を実行
    4. flash 後のリセットを経てブートローダによる暗号化が始まる(完了まで電源を落とさないこと)。シリアル出力の monitor は Ctrl+']' で終了
    5. 暗号化が完了するとブートローダが ESP32 にリセットをかける 〜 暗号化されたイメージでプログラムが起動
    6. 前掲の espefuse.py --port [PORT] summary コマンドを実行し先頭の FLASH_CRYPT_CNT が「1 R/W (0x1)」に変わっていることを確認

ホスト PC 上でのイメージの暗号化とフラッシュメモリへの書き込み

初回の暗号化は上の手順で OK。それ以降にビルドしたイメージについては手元で暗号化を行った上で ESP32 へ転送すればよい。以下の例は app パーティションイメージ分だが、必要に応じて他のイメージも暗号化して書き込む。なお、ビルドしたままの状態のブートローダイメージを再度フラッシュしてはならない。ESP32 側ではブートローダ自体もすでに暗号化されているため。

  1. 更新したプログラムを make してイメージをビルドする。make flash は行わないこと
  2. espsecure.py ユーティリティを使ってホスト PC 上で当該イメージを暗号化する。--address はイメージビルド時点のパーティション定義にそって指定する。 下記例では元のプレーンなイメージが「build/mySimpleApp.bin」であり、出力する暗号化イメージが「build/mySimpleApp_Encrypted.bin」である
    espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x10000 -o build/mySimpleApp_Encrypted.bin build/mySimpleApp.bin
  3. 暗号化したイメージを esptool で ESP32 フラッシュメモリへ書き込んで完了
    esptool.py --port /dev/ttyUSB0 --baud 921600 write_flash 0x10000 build/mySimpleApp_Encrypted.bin

前項の操作を行った記録

手元で実際に前項の操作を行いその様子と結果を記録しました。これが初めての Flash Encryption の操作で、各動画は一発撮りしたものです。

テスト用のプログラムとして以下の「mySimpleApp.c」を使用。LED 点滅ループ中に Flash Encryption の有効無効状態をシリアル出力する内容。

#include <stdio.h>
#include "esp_flash_encrypt.h"
#include "esp_event.h"
#include "esp_log.h"
#include "driver/gpio.h"

const char *secretData = "**** this is secret data ****";
const char *TAG = "TAG";

void app_main()
{
  ESP_LOGI(TAG, "%s", secretData);
  gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
  int sts = 1;
  for (int i = 0; i < 10; i++) {
    if (esp_flash_encryption_enabled()) {
      ESP_LOGI(TAG, "Flash Encryption is enabled");
    } else {
      ESP_LOGI(TAG, "Flash Encryption is disabled");
    }
    gpio_set_level(GPIO_NUM_2, sts);
    sts = !sts;
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}
まず Flash Encryption 有効化前の素の状態のボードに対し make flash monitor を行った様子。"Flash Encryption is disabled" の出力が見える。

動画 A
  

以下、事前生成暗号化キーを使った Flash Encryption 操作の記録。

  1. 暗号化キーをホスト PC 上で生成 〜 生成したキーを ESP32 の efuse 領域へ書き込む
    $ espsecure.py generate_flash_encryption_key ./mykey.bin
    espsecure.py v2.3.1
    
    $ espefuse.py --port /dev/ttyUSB0 burn_key flash_encryption ./mykey.bin
    espefuse.py v2.3.1
    Connecting......
    Write key in efuse block 1. The key block will be read and write protected (no further changes or readback). This is an irreversible operation.
    Type 'BURN' (all capitals) to continue.
    BURN
    Burned key data. New value: 6a 60 3c a7 d0 3f d4 9f b0 a3 f9 ca 77 61 10 47 30 57 80 c5 5c fd bc a0 ce 30 3e 36 b1 b5 ac c4
    Disabling read/write to key efuse block...
    
    
    動画 B
  2. 上記 1. 操作後の efuse の状態
    $ espefuse.py --port /dev/ttyUSB0 summary
    espefuse.py v2.3.1
    Connecting......
    Security fuses:
    FLASH_CRYPT_CNT        Flash encryption mode counter                     = 0 R/W (0x0)
    FLASH_CRYPT_CONFIG     Flash encryption config (key tweak bits)          = 0 R/W (0x0)
    CONSOLE_DEBUG_DISABLE  Disable ROM BASIC interpreter fallback            = 0 R/W (0x0)
    ABS_DONE_0             secure boot enabled for bootloader                = 0 R/W (0x0)
    ABS_DONE_1             secure boot abstract 1 locked                     = 0 R/W (0x0)
    JTAG_DISABLE           Disable JTAG                                      = 0 R/W (0x0)
    DISABLE_DL_ENCRYPT     Disable flash encryption in UART bootloader       = 0 R/W (0x0)
    DISABLE_DL_DECRYPT     Disable flash decryption in UART bootloader       = 0 R/W (0x0)
    DISABLE_DL_CACHE       Disable flash cache in UART bootloader            = 0 R/W (0x0)
    BLK1                   Flash encryption key                              
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -/- 
    BLK2                   Secure boot key                                   
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
    BLK3                   Variable Block 3                                  
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
    
    Efuse fuses:
    WR_DIS                 Efuse write disable mask                          = 128 R/W (0x80)
    RD_DIS                 Efuse read disablemask                            = 1 R/W (0x1)
    CODING_SCHEME          Efuse variable block length scheme                = 0 R/W (0x0)
    KEY_STATUS             Usage of efuse block 3 (reserved)                 = 0 R/W (0x0)
    
    Config fuses:
    XPD_SDIO_FORCE         Ignore MTDI pin (GPIO12) for VDD_SDIO on reset    = 0 R/W (0x0)
    XPD_SDIO_REG           If XPD_SDIO_FORCE, enable VDD_SDIO reg on reset   = 0 R/W (0x0)
    XPD_SDIO_TIEH          If XPD_SDIO_FORCE & XPD_SDIO_REG, 1=3.3V 0=1.8V   = 0 R/W (0x0)
    SPI_PAD_CONFIG_CLK     Override SD_CLK pad (GPIO6/SPICLK)                = 0 R/W (0x0)
    SPI_PAD_CONFIG_Q       Override SD_DATA_0 pad (GPIO7/SPIQ)               = 0 R/W (0x0)
    SPI_PAD_CONFIG_D       Override SD_DATA_1 pad (GPIO8/SPID)               = 0 R/W (0x0)
    SPI_PAD_CONFIG_HD      Override SD_DATA_2 pad (GPIO9/SPIHD)              = 0 R/W (0x0)
    SPI_PAD_CONFIG_CS0     Override SD_CMD pad (GPIO11/SPICS0)               = 0 R/W (0x0)
    DISABLE_SDIO_HOST      Disable SDIO host                                 = 0 R/W (0x0)
    
    Identity fuses:
    MAC                    MAC Address                                       
      = 30:ae:a4:02:59:c0 (CRC 9b OK) R/W 
    CHIP_VER_REV1          Silicon Revision 1                                = 0 -/W (0x0)
    CHIP_VERSION           Reserved for future chip versions                 = 0 -/W (0x0)
    CHIP_PACKAGE           Chip package identifier                           = 0 -/W (0x0)
    
    Calibration fuses:
    BLK3_PART_RESERVE      BLOCK3 partially served for ADC calibration data  = 0 -/W (0x0)
    ADC_VREF               Voltage reference calibration                     = 1100 -/W (0x0)
    
    Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V).
    
    
    $ espefuse.py --port /dev/ttyUSB0 dump
    espefuse.py v2.3.1
    Connecting....
    EFUSE block 0:
    00010080 a40259c0 009b30ae 00000000 00000036 00000000 00000000
    EFUSE block 1:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    EFUSE block 2:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    EFUSE block 3:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    
    
  3. make menuconfig で [Security Features] - [Enable flash encryption boot] を有効化した後で make を実行
    $ make
    CC build/bootloader/bootloader_support/src/bootloader_clock.o
    CC build/bootloader/bootloader_support/src/bootloader_flash.o
    CC build/bootloader/bootloader_support/src/bootloader_init.o
    CC build/bootloader/bootloader_support/src/bootloader_random.o
    CC build/bootloader/bootloader_support/src/bootloader_sha.o
    CC build/bootloader/bootloader_support/src/bootloader_utility.o
    CC build/bootloader/bootloader_support/src/efuse.o
                         (中略)
    CC build/xtensa-debug-module/trax.o
    AR build/xtensa-debug-module/libxtensa-debug-module.a
    LD build/mySimpleApp.elf
    esptool.py v2.3.1
    To flash all build output, run 'make flash' or:
    python /home/t/esp/esp-idf/components/esptool_py/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 115200 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 /home/t/Arduino/my/for_ESP-IDF/mySimpleApp/build/bootloader/bootloader.bin 0x10000 /home/t/Arduino/my/for_ESP-IDF/mySimpleApp/build/mySimpleApp.bin 0x8000 /home/t/Arduino/my/for_ESP-IDF/mySimpleApp/build/partitions_singleapp.bin
    
    
    余談ながら、上の最終行のコマンド例中の「--after hard_reset」の記述が気になり Advanced Options - espressif/esptool Wiki で確認したところ、「--after no_reset」オプションの存在を知った。デフォルトは「--after hard_reset」。esptool 操作後に自動的にリセットがかかると具合のわるい場合があるが、no_reset 指定でそれを回避できることを覚えた。
    動画 C
  4. make flash monitor --- ブートローダが各パーティションを暗号化している状況が見てとれる。その後起動した mySimpleApp からのメッセージ出力が "Flash Encryption is enabled" に変化している
    $ make flash monitor
    Flashing binaries to serial port /dev/ttyUSB0 (app at offset 0x10000)...
    esptool.py v2.3.1
    Connecting........_
    Chip is ESP32D0WDQ6 (revision 0)
    Features: WiFi, BT, Dual Core
    Uploading stub...
    Running stub...
    Stub running...
    Configuring flash size...
    Auto-detected Flash size: 4MB
    Flash params set to 0x0220
    Compressed 25296 bytes to 14703...
    Wrote 25296 bytes (14703 compressed) at 0x00001000 in 1.3 seconds (effective 156.4 kbit/s)...
    Hash of data verified.
    Compressed 140160 bytes to 68713...
    Wrote 140160 bytes (68713 compressed) at 0x00010000 in 6.1 seconds (effective 184.8 kbit/s)...
    Hash of data verified.
    Compressed 3072 bytes to 103...
    Wrote 3072 bytes (103 compressed) at 0x00008000 in 0.0 seconds (effective 1885.7 kbit/s)...
    Hash of data verified.
    
    Leaving...
    Hard resetting via RTS pin...
    MONITOR
    --- idf_monitor on /dev/ttyUSB0 115200 ---
    --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
    ets Jun  8 2016 00:22:57
    
    rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    ets Jun  8 2016 00:22:57
    
    rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    configsip: 0, SPIWP:0xee
    clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
    mode:DIO, clock div:2
    load:0x3fff0018,len:4
    load:0x3fff001c,len:7400
    load:0x40078000,len:0
    ho 12 tail 0 room 4
    load:0x40078000,len:17796
    entry 0x40078724
    I (31) boot: ESP-IDF v3.1-dev-1171-g358c822 2nd stage bootloader
    I (31) boot: compile time 16:28:39
    I (31) boot: Enabling RNG early entropy source...
    I (37) boot: SPI Speed      : 40MHz
    I (41) boot: SPI Mode       : DIO
    I (45) boot: SPI Flash Size : 4MB
    I (49) boot: Partition Table:
    I (52) boot: ## Label            Usage          Type ST Offset   Length
    I (60) boot:  0 nvs              WiFi data        01 02 00009000 00006000
    I (67) boot:  1 phy_init         RF data          01 01 0000f000 00001000
    I (75) boot:  2 factory          factory app      00 00 00010000 00100000
    I (82) boot: End of partition table
    I (86) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x05888 ( 22664) map
    I (103) esp_image: segment 1: paddr=0x000158b0 vaddr=0x3ffb0000 size=0x02284 (  8836) load
    I (107) esp_image: segment 2: paddr=0x00017b3c vaddr=0x40080000 size=0x00400 (  1024) load
    0x40080000: _iram_start at /home/t/esp/esp-idf/components/freertos/xtensa_vectors.S:1685
    
    I (113) esp_image: segment 3: paddr=0x00017f44 vaddr=0x40080400 size=0x080cc ( 32972) load
    I (136) esp_image: segment 4: paddr=0x00020018 vaddr=0x400d0018 size=0x11ccc ( 72908) map
    0x400d0018: _flash_cache_start at ??:?
    
    I (161) esp_image: segment 5: paddr=0x00031cec vaddr=0x400884cc size=0x00668 (  1640) load
    0x400884cc: esp_rom_spiflash_program_page_internal at /home/t/esp/esp-idf/components/spi_flash/spi_flash_rom_patch.c:412
    
    I (162) esp_image: segment 6: paddr=0x0003235c vaddr=0x400c0000 size=0x00000 (     0) load
    I (174) boot: Loaded app from partition at offset 0x10000
    I (174) boot: Checking flash encryption...
    W (179) flash_encrypt: Using pre-loaded flash encryption key in EFUSE block 1
    I (187) flash_encrypt: Setting CRYPT_CONFIG efuse to 0xF
    I (205) flash_encrypt: Disable UART bootloader encryption...
    I (205) flash_encrypt: Disable UART bootloader decryption...
    I (208) flash_encrypt: Disable UART bootloader MMU cache...
    I (214) flash_encrypt: Disable JTAG...
    I (218) flash_encrypt: Disable ROM BASIC interpreter fallback...
    I (237) esp_image: segment 0: paddr=0x00001020 vaddr=0x3fff0018 size=0x00004 (     4) 
    I (237) esp_image: segment 1: paddr=0x0000102c vaddr=0x3fff001c size=0x01ce8 (  7400) 
    I (247) esp_image: segment 2: paddr=0x00002d1c vaddr=0x40078000 size=0x00000 (     0) 
    I (253) esp_image: segment 3: paddr=0x00002d24 vaddr=0x40078000 size=0x04584 ( 17796) 
    I (701) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x05888 ( 22664) map
    I (709) esp_image: segment 1: paddr=0x000158b0 vaddr=0x3ffb0000 size=0x02284 (  8836) 
    I (712) esp_image: segment 2: paddr=0x00017b3c vaddr=0x40080000 size=0x00400 (  1024) 
    0x40080000: _iram_start at /home/t/esp/esp-idf/components/freertos/xtensa_vectors.S:1685
    
    I (716) esp_image: segment 3: paddr=0x00017f44 vaddr=0x40080400 size=0x080cc ( 32972) 
    I (736) esp_image: segment 4: paddr=0x00020018 vaddr=0x400d0018 size=0x11ccc ( 72908) map
    0x400d0018: _flash_cache_start at ??:?
    
    I (762) esp_image: segment 5: paddr=0x00031cec vaddr=0x400884cc size=0x00668 (  1640) 
    0x400884cc: esp_rom_spiflash_program_page_internal at /home/t/esp/esp-idf/components/spi_flash/spi_flash_rom_patch.c:412
    
    I (763) esp_image: segment 6: paddr=0x0003235c vaddr=0x400c0000 size=0x00000 (     0) 
    I (768) flash_encrypt: Encrypting partition 2 at offset 0x10000...
    [0;3oets Jun  8 2016 00:22:57
    
    rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    configsip: 0, SPIWP:0xee
    clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
    mode:DIO, clock div:2
    load:0x3fff0018,len:4
    load:0x3fff001c,len:7400
    load:0x40078000,len:0
    ho 12 tail 0 room 4
    load:0x40078000,len:17796
    entry 0x40078724
    I (30) boot: ESP-IDF v3.1-dev-1171-g358c822 2nd stage bootloader
    I (30) boot: compile time 16:28:39
    I (30) boot: Enabling RNG early entropy source...
    I (36) boot: SPI Speed      : 40MHz
    I (41) boot: SPI Mode       : DIO
    I (45) boot: SPI Flash Size : 4MB
    I (49) boot: Partition Table:
    I (52) boot: ## Label            Usage          Type ST Offset   Length
    I (59) boot:  0 nvs              WiFi data        01 02 00009000 00006000
    I (67) boot:  1 phy_init         RF data          01 01 0000f000 00001000
    I (74) boot:  2 factory          factory app      00 00 00010000 00100000
    I (82) boot: End of partition table
    I (86) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x05888 ( 22664) map
    I (103) esp_image: segment 1: paddr=0x000158b0 vaddr=0x3ffb0000 size=0x02284 (  8836) load
    I (107) esp_image: segment 2: paddr=0x00017b3c vaddr=0x40080000 size=0x00400 (  1024) load
    0x40080000: _iram_start at /home/t/esp/esp-idf/components/freertos/xtensa_vectors.S:1685
    
    I (113) esp_image: segment 3: paddr=0x00017f44 vaddr=0x40080400 size=0x080cc ( 32972) load
    I (136) esp_image: segment 4: paddr=0x00020018 vaddr=0x400d0018 size=0x11ccc ( 72908) map
    0x400d0018: _flash_cache_start at ??:?
    
    I (163) esp_image: segment 5: paddr=0x00031cec vaddr=0x400884cc size=0x00668 (  1640) load
    0x400884cc: esp_rom_spiflash_program_page_internal at /home/t/esp/esp-idf/components/spi_flash/spi_flash_rom_patch.c:412
    
    I (163) esp_image: segment 6: paddr=0x0003235c vaddr=0x400c0000 size=0x00000 (     0) load
    I (175) boot: Loaded app from partition at offset 0x10000
    I (175) boot: Checking flash encryption...
    I (180) flash_encrypt: flash encryption is enabled (3 plaintext flashes left)
    I (188) boot: Disabling RNG early entropy source...
    I (193) cpu_start: Pro cpu up.
    I (197) cpu_start: Starting app cpu, entry point is 0x40080e4c
    0x40080e4c: call_start_cpu1 at /home/t/esp/esp-idf/components/esp32/cpu_start.c:225
    
    I (0) cpu_start: App cpu up.
    I (208) heap_init: Initializing. RAM available for dynamic allocation:
    I (214) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
    I (220) heap_init: At 3FFB32C0 len 0002CD40 (179 KiB): DRAM
    I (227) heap_init: At 3FFE0440 len 00003BC0 (14 KiB): D/IRAM
    I (233) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
    I (239) heap_init: At 40088B34 len 000174CC (93 KiB): IRAM
    I (246) cpu_start: Pro cpu start user code
    I (264) cpu_start: Starting scheduler on PRO CPU.
    I (0) cpu_start: Starting scheduler on APP CPU.
    I (265) TAG: **** this is secret data ****
    I (265) TAG: Flash Encryption is enabled
    I (1265) TAG: Flash Encryption is enabled
    I (2265) TAG: Flash Encryption is enabled
    I (3265) TAG: Flash Encryption is enabled
    I (4265) TAG: Flash Encryption is enabled
    I (5265) TAG: Flash Encryption is enabled
    I (6265) TAG: Flash Encryption is enabled
    I (7265) TAG: Flash Encryption is enabled
    I (8265) TAG: Flash Encryption is enabled
    I (9265) TAG: Flash Encryption is enabled
    
    
    動画 D
  5. 上記 4. 初回暗号化完了後の efuse の状態 -- FLASH_CRYPT_CNT efuse の値が正しく「0x01」に変化
    $ espefuse.py --port /dev/ttyUSB0 summary
    
    espefuse.py v2.3.1
    Connecting.....
    Security fuses:
    FLASH_CRYPT_CNT        Flash encryption mode counter                     = 1 R/W (0x1)
    FLASH_CRYPT_CONFIG     Flash encryption config (key tweak bits)          = 15 R/W (0xf)
    CONSOLE_DEBUG_DISABLE  Disable ROM BASIC interpreter fallback            = 1 R/W (0x1)
    ABS_DONE_0             secure boot enabled for bootloader                = 0 R/W (0x0)
    ABS_DONE_1             secure boot abstract 1 locked                     = 0 R/W (0x0)
    JTAG_DISABLE           Disable JTAG                                      = 1 R/W (0x1)
    DISABLE_DL_ENCRYPT     Disable flash encryption in UART bootloader       = 1 R/W (0x1)
    DISABLE_DL_DECRYPT     Disable flash decryption in UART bootloader       = 1 R/W (0x1)
    DISABLE_DL_CACHE       Disable flash cache in UART bootloader            = 1 R/W (0x1)
    BLK1                   Flash encryption key                              
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -/- 
    BLK2                   Secure boot key                                   
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
    BLK3                   Variable Block 3                                  
      = 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 R/W 
    
    Efuse fuses:
    WR_DIS                 Efuse write disable mask                          = 128 R/W (0x80)
    RD_DIS                 Efuse read disablemask                            = 1 R/W (0x1)
    CODING_SCHEME          Efuse variable block length scheme                = 0 R/W (0x0)
    KEY_STATUS             Usage of efuse block 3 (reserved)                 = 0 R/W (0x0)
    
    Config fuses:
    XPD_SDIO_FORCE         Ignore MTDI pin (GPIO12) for VDD_SDIO on reset    = 0 R/W (0x0)
    XPD_SDIO_REG           If XPD_SDIO_FORCE, enable VDD_SDIO reg on reset   = 0 R/W (0x0)
    XPD_SDIO_TIEH          If XPD_SDIO_FORCE & XPD_SDIO_REG, 1=3.3V 0=1.8V   = 0 R/W (0x0)
    SPI_PAD_CONFIG_CLK     Override SD_CLK pad (GPIO6/SPICLK)                = 0 R/W (0x0)
    SPI_PAD_CONFIG_Q       Override SD_DATA_0 pad (GPIO7/SPIQ)               = 0 R/W (0x0)
    SPI_PAD_CONFIG_D       Override SD_DATA_1 pad (GPIO8/SPID)               = 0 R/W (0x0)
    SPI_PAD_CONFIG_HD      Override SD_DATA_2 pad (GPIO9/SPIHD)              = 0 R/W (0x0)
    SPI_PAD_CONFIG_CS0     Override SD_CMD pad (GPIO11/SPICS0)               = 0 R/W (0x0)
    DISABLE_SDIO_HOST      Disable SDIO host                                 = 0 R/W (0x0)
    
    Identity fuses:
    MAC                    MAC Address                                       
      = 30:ae:a4:02:59:c0 (CRC 9b OK) R/W 
    CHIP_VER_REV1          Silicon Revision 1                                = 0 -/W (0x0)
    CHIP_VERSION           Reserved for future chip versions                 = 0 -/W (0x0)
    CHIP_PACKAGE           Chip package identifier                           = 0 -/W (0x0)
    
    Calibration fuses:
    BLK3_PART_RESERVE      BLOCK3 partially served for ADC calibration data  = 0 -/W (0x0)
    ADC_VREF               Voltage reference calibration                     = 1100 -/W (0x0)
    
    Flash voltage (VDD_SDIO) determined by GPIO12 on reset (High for 1.8V, Low/NC for 3.3V).
    
    
    $ espefuse.py --port /dev/ttyUSB0 dump espefuse.py v2.3.1
    Connecting......
    EFUSE block 0:
    00110080 a40259c0 009b30ae 00000000 00000036 f0000000 000003c4
    EFUSE block 1:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    EFUSE block 2:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    EFUSE block 3:
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    
    
  6. 上記 4. 初回暗号化完了後にあらためてプレーンな app イメージを esptool で書き込んで make monitor -- アプリの起動が適切に弾かれる
    $ esptool.py --port /dev/ttyUSB0 --baud 921600 write_flash 0x10000 build/mySimpleApp.bin; make monitor
    esptool.py v2.3.1
    Connecting....
    Detecting chip type... ESP32
    Chip is ESP32D0WDQ6 (revision 0)
    Features: WiFi, BT, Dual Core
    Uploading stub...
    Running stub...
    Stub running...
    Changing baud rate to 921600
    Changed.
    Configuring flash size...
    Auto-detected Flash size: 4MB
    Compressed 140160 bytes to 68713...
    Wrote 140160 bytes (68713 compressed) at 0x00010000 in 1.5 seconds (effective 730.5 kbit/s)...
    Hash of data verified.
    
    Leaving...
    Hard resetting via RTS pin...
    MONITOR
    --- idf_monitor on /dev/ttyUSB0 115200 ---
    --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
    ets Jun  8 2016 00:22:57
    
    rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    ets Jun  8 2016 00:22:57
    
    rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    configsip: 0, SPIWP:0xee
    clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
    mode:DIO, clock div:2
    load:0x3fff0018,len:4
    load:0x3fff001c,len:7400
    load:0x40078000,len:0
    ho 12 tail 0 room 4
    load:0x40078000,len:17796
    entry 0x40078724
    I (31) boot: ESP-IDF v3.1-dev-1171-g358c822 2nd stage bootloader
    I (31) boot: compile time 16:28:39
    I (31) boot: Enabling RNG early entropy source...
    I (37) boot: SPI Speed      : 40MHz
    I (41) boot: SPI Mode       : DIO
    I (45) boot: SPI Flash Size : 4MB
    I (49) boot: Partition Table:
    I (52) boot: ## Label            Usage          Type ST Offset   Length
    I (60) boot:  0 nvs              WiFi data        01 02 00009000 00006000
    I (67) boot:  1 phy_init         RF data          01 01 0000f000 00001000
    I (75) boot:  2 factory          factory app      00 00 00010000 00100000
    I (82) boot: End of partition table
    E (86) esp_image: image at 0x10000 has invalid magic byte
    W (92) esp_image: image at 0x10000 has invalid SPI mode 255
    E (99) boot: Factory app partition is not bootable
    E (104) boot: No bootable app partitions in the partition table
    user code done
    
    
    前掲のドキュメント記述のように「flash read err, 1000」のエラー出力とリブートのループを予想したがここではこのように「boot: Factory app partition is not bootable」の表示で停止した。このあたりの事情はまだよくわからない
    動画 E
  7. ここでソースコードに小さく変更を加える。メッセージにビックリマークを書き加えただけ
    変更前
    ESP_LOGI(TAG, "Flash Encryption is enabled");

    変更後
    ESP_LOGI(TAG, "Flash Encryption is enabled!!!!");

  8. make を経て PC 上で app イメージを暗号化し esptool でフラッシュメモリへ書き込むと期待通り上の変更が反映された状態で起動
    $ make
    CC build/main/mySimpleApp.o
    AR build/main/libmain.a
    LD build/mySimpleApp.elf
    esptool.py v2.3.1
    To flash all build output, run 'make flash' or:
    python /home/t/esp/esp-idf/components/esptool_py/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 115200 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 /home/t/Arduino/my/for_ESP-IDF/mySimpleApp/build/bootloader/bootloader.bin 0x10000 /home/t/Arduino/my/for_ESP-IDF/mySimpleApp/build/mySimpleApp.bin 0x8000 /home/t/Arduino/my/for_ESP-IDF/mySimpleApp/build/partitions_singleapp.bin
    
    $ espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x10000 -o build/mySimpleApp_encrypted.bin build/mySimpleApp.bin
    espsecure.py v2.3.1
    
    $ esptool.py --port /dev/ttyUSB0 --baud 921600 write_flash 0x10000 build/mySimpleApp_encrypted.bin; make monitor
    esptool.py v2.3.1
    Connecting....
    Detecting chip type... ESP32
    Chip is ESP32D0WDQ6 (revision 0)
    Features: WiFi, BT, Dual Core
    Uploading stub...
    Running stub...
    Stub running...
    Changing baud rate to 921600
    Changed.
    Configuring flash size...
    Auto-detected Flash size: 4MB
    Compressed 140176 bytes to 138083...
    Wrote 140176 bytes (138083 compressed) at 0x00010000 in 2.2 seconds (effective 509.0 kbit/s)...
    Hash of data verified.
    
    Leaving...
    Hard resetting via RTS pin...
    MONITOR
    --- idf_monitor on /dev/ttyUSB0 115200 ---
    --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
    ets Jun  8 2016 00:22:57
    
    rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    ets Jun  8 2016 00:22:57
    
    rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
    configsip: 0, SPIWP:0xee
    clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
    mode:DIO, clock div:2
    load:0x3fff0018,len:4
    load:0x3fff001c,len:7400
    load:0x40078000,len:0
    ho 12 tail 0 room 4
    load:0x40078000,len:17796
    entry 0x40078724
    I (31) boot: ESP-IDF v3.1-dev-1171-g358c822 2nd stage bootloader
    I (31) boot: compile time 16:28:39
    I (31) boot: Enabling RNG early entropy source...
    I (37) boot: SPI Speed      : 40MHz
    I (41) boot: SPI Mode       : DIO
    I (45) boot: SPI Flash Size : 4MB
    I (49) boot: Partition Table:
    I (52) boot: ## Label            Usage          Type ST Offset   Length
    I (60) boot:  0 nvs              WiFi data        01 02 00009000 00006000
    I (67) boot:  1 phy_init         RF data          01 01 0000f000 00001000
    I (75) boot:  2 factory          factory app      00 00 00010000 00100000
    I (82) boot: End of partition table
    I (86) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x0588c ( 22668) map
    I (103) esp_image: segment 1: paddr=0x000158b4 vaddr=0x3ffb0000 size=0x02284 (  8836) load
    I (108) esp_image: segment 2: paddr=0x00017b40 vaddr=0x40080000 size=0x00400 (  1024) load
    0x40080000: _iram_start at /home/t/esp/esp-idf/components/freertos/xtensa_vectors.S:1685
    
    I (113) esp_image: segment 3: paddr=0x00017f48 vaddr=0x40080400 size=0x080c8 ( 32968) load
    I (136) esp_image: segment 4: paddr=0x00020018 vaddr=0x400d0018 size=0x11ccc ( 72908) map
    0x400d0018: _flash_cache_start at ??:?
    
    I (163) esp_image: segment 5: paddr=0x00031cec vaddr=0x400884c8 size=0x0066c (  1644) load
    0x400884c8: esp_rom_spiflash_program_page_internal at /home/t/esp/esp-idf/components/spi_flash/spi_flash_rom_patch.c:412
    
    I (164) esp_image: segment 6: paddr=0x00032360 vaddr=0x400c0000 size=0x00000 (     0) load
    I (175) boot: Loaded app from partition at offset 0x10000
    I (175) boot: Checking flash encryption...
    I (180) flash_encrypt: flash encryption is enabled (3 plaintext flashes left)
    I (188) boot: Disabling RNG early entropy source...
    I (194) cpu_start: Pro cpu up.
    I (197) cpu_start: Starting app cpu, entry point is 0x40080e4c
    0x40080e4c: call_start_cpu1 at /home/t/esp/esp-idf/components/esp32/cpu_start.c:225
    
    I (0) cpu_start: App cpu up.
    I (208) heap_init: Initializing. RAM available for dynamic allocation:
    I (215) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
    I (221) heap_init: At 3FFB32C0 len 0002CD40 (179 KiB): DRAM
    I (227) heap_init: At 3FFE0440 len 00003BC0 (14 KiB): D/IRAM
    I (233) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
    I (240) heap_init: At 40088B34 len 000174CC (93 KiB): IRAM
    I (246) cpu_start: Pro cpu start user code
    I (264) cpu_start: Starting scheduler on PRO CPU.
    I (0) cpu_start: Starting scheduler on APP CPU.
    I (266) TAG: **** this is secret data ****
    I (266) TAG: Flash Encryption is enabled!!!!
    I (1266) TAG: Flash Encryption is enabled!!!!
    I (2266) TAG: Flash Encryption is enabled!!!!
    I (3266) TAG: Flash Encryption is enabled!!!!
    I (4266) TAG: Flash Encryption is enabled!!!!
    I (5266) TAG: Flash Encryption is enabled!!!!
    I (6266) TAG: Flash Encryption is enabled!!!!
    I (7266) TAG: Flash Encryption is enabled!!!!
    I (8266) TAG: Flash Encryption is enabled!!!!
    I (9266) TAG: Flash Encryption is enabled!!!!
    
    
    動画 F
これで FLASH_CRYPT_CNT を気にすることなく Flash Encryption を利用できるようになりました。

Arduino core for ESP32 環境ではどうする?等の実験と結果

ここまでは公式ドキュメントの記述を解釈しながら事前生成暗号化キーを使い ESP-IDF ベースで Flash Encryption の操作を行ってきました。より利用者の多い Arduino core for ESP32 環境での事情が気になります。現時点ではこの点に関する的確な情報が見当たらないため手元で調査と実験を行いました。

実験 1: Flash Encryption 有効状態でのバックアップ・レストアは可能?

前項までの確認の結果、現在手元には FLASH_CRYPT_CNT efuse の値が 0x01 の ESP32 ボードがある。ここにはアプリ「mySimpleApp」の本体のイメージをはじめ暗号化された一式が格納されている。

まず素朴に、この状態でフラッシュメモリ全領域のバックアップとレストアが可能であるか否かに関心を持った。 さっそく esptool を使い下記要領でバックアップとレストアを試したところ、Flash Encryption 不使用時と同様に問題なく処理できることを確認した。物理的な読み書きの際に間で細工をしたりはしないらしい。まずはひと安心。

esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0 0x400000 flash4MB.bin

esptool.py --port /dev/ttyUSB0 --baud 921600 write_flash 0 flash4MB.bin

実験 2: パーティションイメージを復号してみる

次に、esptool で読み出した所定の暗号化ずみパーティションを espsecure ユーティリティ の decrypt_flash_data コマンドで正しく復号可能であることを確認した。こういった整合性は一連の文脈の中で適切に保たれている。 以下は暗号化されたパーティションテーブルをホスト PC 上に読み出して復号する例。decrypt_flash_data コマンドにおいても --address 指定が必須であることに注意が必要。

esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x8000 0xc00 partition_table_Encrypted.bin

espsecure.py decrypt_flash_data --keyfile ./mykey.bin --address 0x8000 -o partition_table_plain.bin partiiion_table_Encrypted.bin

なお、サイズ不定のブートローダーについては、プロジェクトのビルド時に build/bootloader/ 下に生成された暗号化前の bootloader.bin のファイルサイズ(ここでは 25,296 バイト)を指定して暗号化イメージを採取。上記と同じ要領で復号可能だった。

esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x1000 25296 bootloader_Encrypted.bin

espsecure.py decrypt_flash_data --keyfile ./mykey.bin --address 0x1000 -o bootloader_plain.bin bootloader_Encrypted.bin

実験 3: 全パーティションのイメージを採取・復号・再暗号化して書き戻すと?

上のようにパーティションテーブルとブートローダのイメージの取得と復号の確認を終えたところで、他のパーティションイメージもすべて採取し、適宜復号と再暗号化を行った上で ESP32 へ書き戻してやれば普通にプログラムが動作するのではないかと考え実際に手を動かして試してみた。

まずパーティション定義を参照し各パーティションイメージを個別に採取する。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 1M,

前掲のように nvs は暗号化されておらず、読み出し不可の phy は中身がすべて 0xFF だった。後者はいずれにせよ mySimpleApp では使用していないためひとまずこのままにしておく。Type = app の factory パーティションについてのみ復号を行った。実行したコマンドを以下に控える。

esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x9000 0x6000 nvs.bin
esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0xf000 0x1000 phy_init.bin
esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x10000 0x100000 factory_Encrypted.bin

espsecure.py decrypt_flash_data --keyfile ./mykey.bin --address 0x10000 -o factory_plain.bin factory_Encrypted.bin

次に、復号したブートローダ, パーティションテーブル, factory パーティションをあらためて暗号化する。

espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x1000 -o bootloader_reEncrypted.bin bootloader_plain.bin
espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x8000 -o partition_table_reEncrypted.bin partition_table_plain.bin
espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x10000 -o factory_reEncrypted.bin factory_plain.bin

準備を終えた一式を下記コマンドでフラッシュメモリへ書き戻した。

esptool.py --port /dev/ttyUSB0 --baud 921600 write_flash \
    0x1000 bootloader_reEncrypted.bin \
    0x8000 partition_table_reEncrypted.bin \
    0x9000 nvs.bin \
    0xf000 phy_init.bin \
    0x10000 factory_reEncrypted.bin

この結果、アプリケーション mySimpleApp は問題なく動作した。当初複雑に感じられた Flash Encryption は案外素直な構造らしい。

実験 4: Arduino core for ESP32 環境で開発したアプリ一式を移行してみる

ここまでの結果に続けて、ESP-IDF ではなく Arduino core for ESP32 環境でビルドしたアプリケーションイメージ一式について Flash Encryption 環境での取り回しを試みた。

実験用の題材として Arduino IDE 環境で直近に作成した Google Home に発話させるエージェントプログラムを使った。アプリ内で SPIFFS を利用していることも実験を行う上で何となく好都合。パーティションテーブルは以下の内容。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x140000,
app1,     app,  ota_1,   0x150000,0x140000,
eeprom,   data, 0x99,    0x290000,0x1000,
spiffs,   data, spiffs,  0x291000,0x16F000,

手元では同アプリを Flash Encryption を適用していない素の ESP32 ボードで運用中。

まず、この非 Flash Encryption なボード A をホスト PC へ接続し、フラッシュメモリからパーティションイメージ一式を採取する。

esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x9000 0x5000 nvs.bin
esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0xe000 0x2000 otadata.bin
esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x10000 0x140000 app0.bin
esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x150000 0x140000 app1.bin
esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x290000 0x1000 eeprom.bin
esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x291000 0x16F000 spiffs.bin

esptool.py --port /dev/ttyUSB0 --baud 921600 read_flash 0x8000 0xC00 partition_table.bin

ブートローダはサイズ不定だが、Arduino core for ESP32 の場合、[arduino]/hardware/espressif/esp32/tools/sdk/bin/ ディレクトリ下に SPI モード + Flash Frequency 別に用意されているためこれを利用できる。

$ pwd
/home/t/app/arduino-1.8.5/hardware/espressif/esp32/tools/sdk/bin
$ ls -l
合計 128
-rw-rw-r-- 1 t t 15088  7月 30 21:42 bootloader_dio_40m.bin
-rw-rw-r-- 1 t t 13696  7月 30 21:42 bootloader_dio_80m.bin
-rw-rw-r-- 1 t t 13696  7月 30 21:42 bootloader_dout_40m.bin
-rw-rw-r-- 1 t t 13696  7月 30 21:42 bootloader_dout_80m.bin
-rw-rw-r-- 1 t t 15088  7月 30 21:42 bootloader_qio_40m.bin
-rw-rw-r-- 1 t t 15072  7月 30 21:42 bootloader_qio_80m.bin
-rw-rw-r-- 1 t t 15088  7月 30 21:42 bootloader_qout_40m.bin
-rw-rw-r-- 1 t t 15072  7月 30 21:42 bootloader_qout_80m.bin

本アプリは "QIO" + "80MHz" の指定でビルドしているため「bootloader_qio_80m.bin」をコピーして使う。

  

暗号化の対象であるブートローダー, パーティションテーブル および app0, app1 パーティションのイメージを Flash Encryption 適用ずみの ESP32 ボード B のキーを使って暗号化する。

espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x1000 -o bootloader_Encrypted.bin bootloader_qio_80m.bin
espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x8000 -o partition_table_Encrypted.bin partition_table.bin
espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x10000 -o app0_Encrypted.bin app0.bin
espsecure.py encrypt_flash_data --keyfile ./mykey.bin --address 0x150000 -o app1_Encrypted.bin app1.bin

ここで Flash Encryption 適用ずみの ESP32 ボード B をホスト PC につなぎ替えてから、以下の要領で各パーティションイメージを書き込む。

esptool.py --port /dev/ttyUSB0 --baud 921600 write_flash \
    0x1000 bootloader_Encrypted.bin \
    0x8000 partition_table_Encrypted.bin \
    0x9000 nvs.bin \
    0xe000 otadata.bin \
    0x10000 app0_Encrypted.bin \
    0x150000 app1_Encrypted.bin \
    0x290000 eeprom.bin \
    0x291000 spiffs.bin

以上の手順により、Arduino core for ESP32 環境でビルドを行い素の ESP32 ボード A 上で稼働中の当該アプリが、Flash Encryption 適用ずみのボード B 上で正常に動作した

Flash Encryption を道具として普段使いするために

上の結果を受けて、Flash Encryption を道具として日常的に利用する上でのポイントを想定した。

  • あらかじめ「初回の暗号化」を経て FLASH_CRYPT_CNT = 0x01 状態となったボードを資材として用意しておく
  • 普段は素のボートを使って Arduino IDE 環境なり ESP-IDF 環境なりで普通にプログラム開発を行う
  • 自分の手を離れた場所にボードを設置する場合は上の要領で素のボードから Flash Encryption 有効化ずみボードへイメージを移行して利用する
       ESP32 Board A                         ESP32 Board B
    +------------------+  copy & encrypt  +------------------+
    | Flash Encryption | partition images | Flash Encryption |
    |     DISABLED     | ===============> |     ENABLED      | 
    +------------------+                  +------------------+
    

理屈がわかってしまえば移行方法そのものは単純だが、手順が面倒なので適切に一括操作を行うために Python スクリプトを用意した。Linux, MacOSX, Windows の各環境に対応。なお、前項の実験ではブートローダのイメージを SDK ディレクトリからコピーして利用したが、ブートローダコードは固定長ではないもののサイズの上限は 0x7000 バイトであり、素のボードのフラッシュメモリオフセット 0x1000 から 0x7000 バイト分を読み出して処理したところ問題なく動作したためこの方法を採った。これなら元アプリの開発環境が何であるかにかかわらず対応できる。

  1. Flash Encryption の無効なボード A のフラッシュメモリからブートローダイメージをホスト PC へ採取
  2. 同じくパーティションテーブルイメージを採取
  3. 上のパーティションテーブルの内容に基づき各パーティションイメージを採取
  4. 暗号化の必要なイメージを Flash Encryption の有効なボード B 用のキーで暗号化する
  5. イメージ一式をボード B のフラッシュメモリへ書き込む

以下の動画はこのスクリプトを使って前掲の「Google Home に発話させるエージェントプログラム」を稼働させている素のボード A のフラッシュメモリパーティションイメージ一式を暗号化有効なボード B へ移行した様子。

前半: 素のボード A のフラッシュメモリからの各パーティションイメージ採取と暗号化完了まで

後半: 上のイメージを Flash Encryption の有効なボード B に書き込んで当該アプリの稼働を確認

残っている疑問

今回、所定のボードで Flash Encryption を有効化するまでの過程ではドキュメントの内容どおりに ESP-IDF 環境で make menuconfig での暗号化オプション有効化を経て make && make flash を実行する手順を踏んだが、ボード側のお膳立てとしては FLASH_CRYPT_CNT efuse の値を 0x01 にするだけで事足りる可能性も想像される。

もし仮にそうであれば、素の状態のボードに対し事前生成暗号化キーを書き込んだ上で 前掲のように「espefuse.py --port [PORT] burn_efuse FLASH_CRYPT_CNT」コマンドを一度発行するだけで準備 OK かもしれない。手元ではまだ試せていないが関心はある。この件もおって確認したい。


以上、長いメモでした。いろいろ興味深い機構です。


(tanabe)
klab_gijutsu2 at 19:32
この記事のURLComments(0)IoT 
2018年08月08日

Google Home でローカルの MP3 ファイルをプレイリスト再生する方法

はてなブックマークに登録

前回の記事では ほりひろ 様による esp8266-google-home-notifier ライブラリと ESP32 ボードを使って構成した Google Home 用キャストエージェントとその利用例を紹介しました。記事ではあわせて同ライブラリをプライベートにカスタマイズした内容として以下を挙げました

  1. 元の google-home-notifier に存在する ip 関数を追加
  2. 元の google-home-notifier に存在する play 関数 (public)を追加
  3. play 関数による Google Home での MP3 データ再生終了を検知するためのコールバック機構を追加

今回はみっつめの話題について紹介します。

話のきっかけ

Google Home Mini で Spotify など試しているうちに、昔買った音楽 CD をこういう手軽さで聴くことができればと思うようになりました。そのためには楽曲データを MP3 形式等に変換して Google Play Music へアップロードすれば無料で実現できると随所で紹介されています。なかなか魅力的なサービスではありますが、その一方で Google 側の利用規約とのかねあいが気になりました。

手元では日常的に Google の多くの優れたサービスの便宜を享受しており、ひとりのユーザとして同社には敬意と信頼感を持っています。しかし、自分にすべての権利のあるデータであればともかく、自分には所有権しかない楽曲のデータを丸ごとこの規約のもとへ預けることには個人的にためらいがありました。そこで注目したのが google-home-notifier の MP3 データ再生機能です。

google-home-notifier には URL で指定された MP3 データを Google Home へキャストすることのできる play 関数が用意されています。そのため、LAN 上にローカル Web サーバをを立ててそこへ所定の MP3 データを配置し、そのアドレスをこの関数へ渡せば手元の環境からデータを持ち出すことなく Google Home で再生することが可能です。このやり方なら市販の楽曲に対する自分の権利の範囲を踏み越える心配はないでしょう。

ただ、play 関数は一件のデータをワンショットでキャストすることを前提とするもので複数データの連続再生には対応していません。そのため何曲かを順番に流すといったことはできないのが残念です。そこで、この部分に手を加え、所定のプレイリストを参照して複数の MP3 データの順次再生・シャッフル再生ができるようにしてみました。

まずその動作の様子を動画で紹介します。自宅では向かって左側の Raspberry Pi Zero W をローカル Web サーバとして運用しています。このデモでは soundorbis 様によるフリージングル曲を使用させて頂いています。

動画:1分15秒
  

ちなみにここでは以下の IFTTT アプレット (クリックで可読大表示) 経由でローカル Web サーバから再生リスト「demo.txt」内容を読み込んで処理を行っています。
トリガー
アクション

再生リストとその周辺の話題は末尾の「エージェントプログラム」の項に記述しています。
以下、実現に至るまでの経緯を控えます。

対応方法の調査

google-home-notifier の処理内容

まず google-home-notifier の処理を追うことから始めました。以下はそのまとめです。esp8266-google-home-notifier はオリジナルの google-home-notifier が堅実に移植されたライブラリであるためここでは後者をターゲットとしています。

初期設定
  • 呼び出し側から ip() 関数により対象とする Google Home デバイスの IP アドレスと言語が指定されればそれを設定
  • 呼び出し側から device() 関数により対象 Google Home デバイスのデバイス名と言語が指定されればそれを保持

主処理

  • notify() 関数は渡されたテキストを所定の言語で TTS 処理した結果の MP3 ファイルの URL を対象 Google Home デバイスへキャストする
  • play() 関数は指定された MP3 ファイルの URL をを対象 Google Home デバイスへキャストする
  • notify(), play() はいずれも呼び出された時点で対象 Google Home デバイスの IP アドレスが未設定状態であれば前出 device() 関数経由で保持しているデバイス名で mDNS 照会により対象 Google Home デバイスの IP アドレスを得る  =>  GitHub: mdns

キャストの手順

注目すべき点と考え方

ポイントは、上記「キャストの手順」の最後の「load() が終わったら各仮想チャネルを close してコネクションを切断」という部分にあります。該当箇所のソースコードを引用します。

  • google-home-notifier.js (#L88-L96)
                  :
    var onDeviceUp = function(host, url, callback) {
      var client = new Client();
      client.connect(host, function() {
        client.launch(DefaultMediaReceiver, function(err, player) {
    
          var media = {
            contentId: url,
            contentType: 'audio/MP3',
            streamType: 'BUFFERED' // or LIVE
          };
          player.load(media, { autoplay: true }, function(err, status) {
            client.close();
            callback('Device notified');
          });
                  :
    

このように、Google Home 側の DefaultMediaReceiver へ再生対象データの引き渡し(player.load())を終えるとプログラムは直ちにデバイスとの接続を閉じそのまま終了しています。あとは Google Home まかせということになりますが、Google Home はデータ再生中に別のデータのキャストが指示されると現在の再生の終了を待つことなくあっさり新しいデータの再生を開始します。そのため、単に google-home-notifier を繰り返し実行しても、その都度データ再生が中断され別データの再生に移るだけで、プレイリスト再生のように「一件の再生が終了〜次のデータを再生」という動きにはなりません。

一方、手元で上記の player.load() 後も接続を維持した状態で観察したところ、Google Home 側でデータ再生が終了すると前掲の仮想チャネル「urn:x-cast:com.google.cast.media」経由でデバイス側からプログラムへ下記要領のメッセージが送出されることを確認しました。

{"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"currentItemId":1,"idleReason":"FINISHED"}],"requestId":0}

このように、接続を維持していればプログラムはデバイス側の再生終了の捕捉が可能であることがわかりました。 この件を含めここまでの一連の事情を踏まえると、google-home-notifier でデータの連続再生を実現するために必要な措置は大きく次のふたつであることがわかります。

  • デバイスへの再生指示を終えたあともデバイスとの接続を維持する
  • デバイスから上記の再生終了メッセージを受け取ったら次のデータの再生を指示する

なお、Google Cast の仕様にはデータの連続再生を行うためのネイティブの機構が存在するのではないかとも思うのですが、手元では今のところその方面の具体的な情報に行き着いておらず、いずれにせよここではあくまでも現行の google-home-notifier の機能を拡張することにしました。

実装まわりの話題

方針

実装にあたっては次の方針を基本としました。

  • 複数 MP3 データの連続再生はあくまでも拡張機能として取り回す。従来の TTS 機能および単一の MP3 再生機能との互換性を保ち両者の処理を衝突させない
  • デバイスからの再生終了メッセージを受け取るためのコネクション維持と監視は他の処理を止めないためにバックグラウンドタスク内で実施する
  • バックグラウンドタスクから再生終了通知を受け取るためにコールバック機構を設置する。ライブラリ側へ追加するのはこの「一件の MP3 データ再生が終了した場合、または中断・スキップされた場合にコールバック関数経由で通知する」処理にとどめ、それ以外の処理はアプリケーション側の要件としてエージェントプログラムへ実装する
  • つまりライブラリ側の改変は必要最小限の内容とする

処理のイメージ

左側のフローは手元での p8266-google-home-notifier とエージェントプログラムによる処理内容を要約したものです。右側はそこに調査から得られた情報を加味して MP3 データのリスト再生機能を追加したイメージで、ざっくり、赤文字の「キャスト処理」「再生監視タスク」の部分をライブラリ側の処理要素と想定しています。なお灰色のブロックは右フローとの共通部分です。 (クリックで可読大表示)

Google Home からのメッセージについて

上記右フローの「再生監視タスク」パート内の条件分岐部分でも触れていますが、データ再生の監視中には Google Home デバイスから仮想チャネル経由でさまざまなメッセージが送られてきます。手元での観察結果から、今回の実装では以下のメッセージを利用しています。

  • (右フロー中の「※1」) データの再生終了は、前掲のとおり当該デバイスからの「urn:x-cast:com.google.cast.media」チャネル経由での以下のメッセージによって判定できる。これを受け取ったらエージェント側は次の曲の再生へ移行する
    {"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"currentItemId":1,"idleReason":"FINISHED"}],"requestId":0}
  • (右フロー中の「※2」) デバイスが他のアプリ等とのキャストセッションを開始した場合には「urn:x-cast:com.google.cast.tp.connection」チャネルから以下のメッセージが届く。これを受け取ったらエージェント側は現在の連続再生処理を停止する
    {"type":"CLOSE"}
    • なお、Google Cast 機構においてはデバイス側との接続の維持・確認のために上記チャネル上で相互に PING, PONG メッセージの応酬を継続的に実施することが流儀のひとつ。バックグラウンドタスクでの待機中は 5秒毎に PING を送ることにしたが、タイミングによってはデバイス側から PING が届くこともある。その場合は PONG を送る
      {"type":"PING"}
      {"type":"PONG"}
  • (右フロー中の「※3」) ユーザが「OK, Google。ストップ」「OK, Google。やめて」等の再生停止を求めるフレーズを発した場合は「urn:x-cast:com.google.cast.media」チャネル経由でデバイスから下記要領のメッセージが届く。これを受け取ったらエージェント側は連続再生を停止する
    {"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PAUSED","currentTime":1.880214,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":388729138}
  • (右フロー中の「※4」) ユーザが「OK, Google。スキップ」「OK, Google。次の曲」等のフレーズで次データへのスキップをリクエストした場合は「urn:x-cast:com.google.cast.media」チャネル経由でデバイスから下記要領のメッセージが届く。この状況でのステータスは「"playerState":"PLAYING"」かつ、「"requestId":」の値が 0 以外となる。 これを受け取ったらエージェント側は次のデータの再生へ移行する
    {"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":15.665338,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":388729139}
    なお「"requestId":0」のメッセージがデータの再生開始時点で一度発行されるため上記と混同しないよう注意
    {"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"PLAYING","currentTime":1.384362,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"activeTrackIds":[],"currentItemId":1,"repeatMode":"REPEAT_OFF"}],"requestId":0}

ソースコード

カスタマイズした esp8266-google-home-notifier

オリジナルの esp8266-google-home-notifier ライブラリのコードへプライベートに手を加えています。変更箇所は「#ifdef TANABE」部分で、fork した GitHub リポジトリへ反映しています。変更内容はこの記事の冒頭、および前回の記事で触れたとおりです。

$ git clone -b private https://github.com/mkttanabe/esp8266-google-home-notifier.git

また、同リポジトリの examples/esp32/SimpleUsage/ 下に play 関数用のサンプルスケッチを加えました。どちらもオリジナルの SimpleUsage.ino のコピーを小さく書き替えたものです。

このふたつの追加サンプルを実行した様子の動画です。
  

エージェントプログラム

Arduino core for the ESP32 環境で作成したエージェントの Adruino スケッチです。前回分と互換性のある機能拡張版です。

  • GoogleHomeNotifierESP32AgentEx - github.com/mkttanabe

    GoogleHomeNotifierESP32Agent.ino の 以下の箇所を環境にあわせて書き替える
    //------- ユーザ定義 ------------------

    // Google Home デバイスの IP アドレス, デバイス名
    // 有効にすれば指定 IP アドレスを直接使用
    // 無効にすれば指定デバイス名で mDNS 照会
    //#define USE_GH_IPADRESS

    IPAddress myGoogleHomeIPAddress(192,168,0,110);
    #define myGoogleHomeDeviceName "room02"

    // WiFi アクセスポイント
    #define ssid "ssid"
    #define password "pass"

    // Beebotte 情報
    #define mqtt_host "mqtt.beebotte.com"
    #define mqtt_port 8883
    #define mqtt_topic "test01/msg"
    #define mqtt_pass "token:" "token_************"

    // MP3 データを配置する Web サーバ
    #define dataServer "192.168.0.127"
    #define dataServerPort 80
    // MP3 データの URL
    #define MP3DataFmt "http://" dataServer "/sound/%s.mp3"
    // MP3 再生リストのパス
    #define MP3ListFmt "/sound/list/%s.txt"
    //-------------------------------------

    上の記述の場合、自分の Beebotte アカウントの Channel "test01", Resource "msg" のメッセージの data キーの値に応じて以下が行われる
    • data 値の先頭文字がアスタリスクであれば音声合成用のテキストとみなして google-home-notifier の TTS 処理にかける
      • 例) {"data":"*こんにちは"} => 「こんにちは」と発話
    • data 値の先頭文字がアスタリスクまたは "@" でなければ MP3 ファイル名とみなし所定の Web サーバの URL に編集して再生
      • 例) {"data":"file01"} => "http://192.168.0.127/sound/file01.mp3" をキャスト
    • data 値の先頭文字が "@" であれば MP3 データ再生リスト名とみなし所定の Web サーバの URL に編集して読み込み、記述されている各 MP3 データエントリを再生
      • 例) {"data":"@demo"} => "http://192.168.0.127/sound/list/demo.txt" を再生リストとして読み込んで処理
        • 下記の指定があればシャッフル再生を行う
          {"data":"@demo,1"} , {"data":"@list01,ランダム"}
        • 上記以外ならリスト上の記述順に再生
      • 再生リストの記述例
        demo.txt
        # 各行の '#' 以降はコメントとして扱われる
        # 空行は無視、有効行先頭末尾のスペース・タブは除去される

        # soundorbis 様によるフリー BGM 作品より
        #
        # 【フリーBGM】リコーダージングル【01〜05】
        # https://www.youtube.com/watch?v=ztm1CSZEpY8
        #
        # 利用規約
        # https://www.soundorbis.net/license
        #

        demo/[1]nc150689
        demo/[2]nc150690
        demo/[3]nc150691
        demo/[4]nc150692
        demo/[5]nc150693

    なお、上の「ユーザ定義」箇所において「#define USE_GH_IPADRESS」が無効な場合は前回版と同様に mDNS 照会によりデバイス名から IP アドレスを取得するが、このやや時間のかかる照会のコストを軽減するために、今回の拡張版においては一旦取得した IP アドレスを ESP32 のフラッシュメモリ領域 (SPIFFS) へ記録し次回はまずそれを参照する処理を加えている。当該アドレスへアクセスできない場合や当該アドレスのデバイスを Google Home と識別できない場合はあらためて照会と記録を行う。詳細は DeviceAddress.cpp を参照のこと

こういった経緯を経て形になったのが冒頭のデモ動画で稼働しているエージェントです。Raspberry Pi Zero W のローカル Web サーバとペアでとても快適に利用しています。


(tanabe)
2018年08月03日

Google Home を拠点間の双方向コミュニケーションに利用する

はてなブックマークに登録

ESP32 版 google-home-notifier のこと

google-home-notifier の ESP8266 移植版「esp8266-google-home-notifier」の作者である ほりひろ 様が、今年(2018年)6月に ESP32 への対応を実施されました。node.js プログラムであるオリジナルの google-home-notifier を実行可能なもっとも小振りなプラットフォームはラズベリーパイですが、さらにコンパクトで消費電力の少ない ESP32 ボードで自由に Google Home へのキャストを実現できることには大きな魅力があります。自宅ではこの素晴らしいライブラリを使って MQTT メッセージをトリガーにキャスティングを行う内容のエージェントを構成し様々な要件で利用させて頂いています。

「声のアバター」によるやりとり?

そんな中で先日ふと、google-home-notifier の「Google Home で任意の音を出せる機能」と、Google Home 側の「音声で所定の処理を実行できる機能」の組み合わせを、離れた拠点間での定型的なコミュニケーションに利用できるのではないか? と思い立ちました。図のようなイメージです。

双方のエージェントがそれぞれ MQTT ブローカー上の別のトピックを Subscribe しておき、必要に応じて相手のトピックへメッセージを Publish することで現地の Goole Home が所定のアナウンスを行います。その内容に対する所定のフレーズでの応答で相手へ向けての処理を発動する形にしておけば、いわば「声のアバター」を通じて両者間のやりとりが成立すると考えました。同じく音声による連絡方法であっても電話とは異なりこのやり方には直接的な拘束感やある種の重さがないため双方ともより手軽により淡々と利用できるでしょう。

試作のシナリオ

いろいろ使途がありそうですが、手元ではまず個人的に現在もっとも身近なテーマである「高齢者世帯の安否確認」を想定したシナリオを形にしてみました。以下の内容です。

A:見守り側
B:高齢者世帯側

  • A => Google Home A:「ねえ Google、安否を確認」
    • Google Home A => A:『はい、これから声をかけてみます』

  • Google Home B => B:『身のまわりは大丈夫ですか?問題がない場合は "ねえ Google、順調です" と話しかけて下さい。もし何かご相談があれば、"ねえ Google、コールして" と話しかけて下さい』

    • パターン 1: B => Google Home B:「ねえ Google、順調です」
      • Google Home B => B:『それはよかったです。さっそく大丈夫と伝えておきますね』
        • Google Home A => A:『先方から "元気です"と音声連絡がありました。メールを確認して下さい』
        • A あてにメールが届く


    • パターン 2: B => Google Home B:「ねえ Google、コールして」
      • Google Home B => B:『はい、すぐに電話をするようにと伝えます』
        • Google Home A => A:『先方から "電話してほしい"と音声連絡がありました。メールを確認して下さい』
        • A あてにメールが届き携帯電話が数回コールされる

相手が高齢者でもあるため Google Home からのアナウンスにはとりわけ「聞き取りやすさ」が求められます。定型の文言なので毎回音声合成を行う必然性もなく、ここでは、google-home-notifier の notify 関数から呼び出される Google 翻訳 TTS を使うのではなく、複数のオンライン TTS サービスを試した中で個人的にもっとも自然に感じられた音声を収録した MP3 データを play 関数で再生することにしました。

動作の様子

後出のプログラムリソース一式を使って二組の「Google Home Mini + エージェント」間でやりとりを行った様子の動画です。

(パターン 1: 「安否を確認」 〜 「順調です」 1分17秒

(パターン 2: 「安否を確認」 〜 「コールして」) 1分16秒

リソース

esp8266-google-home-notifier のカスタマイズ

前述のように手元では作成したエージェントをさまざまな要件で利用しており、この間にオリジナルの esp8266-google-home-notifier ライブラリのコードへ何点かプライベートに手を加えています。変更箇所は「#ifdef TANABE」部分で、fork した GitHub リポジトリへ反映しています。

$ git clone -b private https://github.com/mkttanabe/esp8266-google-home-notifier.git

変更内容

  1. 元の google-home-notifier に存在する ip 関数を追加
    • Google Home デバイスアドレスの mDNS 照会に比較的時間がかかるため IP アドレスの直指定もできると嬉しいため
  2. 元の google-home-notifier に存在する play 関数 (public)を追加
    • 手元では TTS 機能だけでなく MP3 再生機能も必要であるため
  3. play 関数による Google Home での MP3 データ再生終了を検知するためのコールバック機構を追加
    • 元の google-home-notifier にもないが欲しいと思っていた機能。理由は後日あらためて、、

エージェントプログラム

Arduino core for the ESP32 環境で作成したエージェントの Adruino スケッチです。MQTT ブローカーに Beebotte を使用しています。

  • GoogleHomeNotifierESP32Agent - github.com/mkttanabe
    GoogleHomeNotifierESP32Agent.ino の 以下の箇所を環境にあわせて書き替える
    //------- ユーザ定義 ------------------
    // Google Home デバイスの IP アドレス, デバイス名
    // 有効にすれば指定 IP アドレスを直接使用
    // 無効にすれば指定デバイス名で mDNS 照会
    #define USE_GH_IPADRESS

    IPAddress myGoogleHomeIPAddress(192,168,0,121);
    #define myGoogleHomeDeviceName "room01"

    // WiFi アクセスポイント
    #define ssid "ssid"
    #define password "pass"

    // Beebotte 情報
    #define mqtt_host "mqtt.beebotte.com"
    #define mqtt_port 8883
    #define mqtt_topic "test01/msg"
    #define mqtt_pass "token:" "token_************"

    // MP3 データを配置する Web サーバとデータパス
    #define dataServer "192.168.0.126"
    #define dataServerPort 80
    #define MP3DataFmt "http://" dataServer "/sound/%s.mp3"

    //-------------------------------------

    上の記述の場合、自分の Beebotte アカウントの Channel "test01", Resource "msg" のメッセージの data キーの値に応じて以下が行われる.
    • data 値の先頭文字がアスタリスクであれば音声合成用のテキストとみなして google-home-notifier の TTS 処理にかける
      • 例) {"data":"*こんにちは"} => 「こんにちは」と発話
    • data 値の先頭文字がアスタリスクでなければ MP3 ファイル名とみなし所定の Web サーバスペースの URL を編集して再生
      • 例) {"data":"file01"} => "http://192.168.0.126/sound/file01.mp3" をキャスト

用意した IFTTT アプレット

今回の試作では、トリガーに Google Assistant サービス、アクションに WebHooks サービスをアサインしたみっつの IFTTT アプレットを使っています。 ※ いずれもクリックで可読大表示

  • 問い合わせフレーズ「安否を確認」対向のアプレット
    アクションで Beebotte 上の相手側 "test02/msg" トピックへ "AnpiQuery.mp3" のキャスト再生を指示するメッセージを Publish
    トリガー
    アクション
  • 応答フレーズ「順調です」および「コールして」に対向のアプレット
    アクションで次項の Google Apps Script による Web アプリコードを所定のパラメータを添えて実行
    トリガー
    アクション
    トリガー
    アクション

Google Apps Script による Web アプリコード

上のふたつの応答用アプレットから呼び出している GAS コードです。

  • 「順調です」の場合
    アプレットから渡される res パラメータの値は「safe」。 Beebotte 上の相手側 "test01/msg" トピックへ "AnpiSafe.mp3" のキャスト再生を指示するメッセージを Publish して「元気です」と Gmail を送信
  • 「コールして」の場合
    アプレットから渡される res パラメータの値は「doCall」。まず Twillio 経由で相手の携帯電話を数回コールしてから Beebotte 上の相手側 "test01/msg" トピックへ "AnpiDoCall.mp3" のキャスト再生を指示するメッセージを Publish して「電話がほしい」と Gmail を送信


/*
  AnpiResponse
  2018-07
*/
var ADMIN = "***********@gmail.com";

function doGet(e) {
  return  ContentService.createTextOutput("??");
}

function doPost(e) {
  return doIt(e);
}

function doIt(e) {
  var msg, mp3Name;
  var res = e.parameter.res;
  var where = e.parameter.where;

  if (res == "safe") {
    msg = "元気です";
    mp3Name = "AnpiSafe";
  } else if (res == "doCall") {
    msg = "電話がほしい";
    mp3Name = "AnpiDoCall";
    doPhoneCall(); // twilio
  } else {
    return  ContentService.createTextOutput("res not found");
  }

  doPublish(mp3Name); // MQTT

  // gmail
  doSendMail(ADMIN, "Anpi", ADMIN, ADMIN,
           "安否連絡 [" + res + "]", 
           curDate() + " " + curTime() + "\n" +
           where + " より 「" + msg + "」と連絡がありました");

  return  ContentService.createTextOutput("OK");
}

function curDate() {
  var d = new Date();
  return d.getFullYear() + '-' +
    ('00' + (d.getMonth()+1)).slice(-2) + '-' + ('00' + d.getDate()).slice(-2);
}

function curTime() {
  var d = new Date();
  return ('00' + d.getHours()).slice(-2) + ':' +
    ('00' + d.getMinutes()).slice(-2) + ':' +  ('00' + d.getSeconds()).slice(-2);
}

function doPhoneCall() {
  var url = "https://api.twilio.com/2010-04-01/Accounts/****************/Calls";
  var data = "To=%2B************&From=%2B***********0&Url=http://demo.twilio.com/docs/voice.xml&Timeout=10";
  var options = {
    method: "POST",
    headers: {
      "Authorization":"Basic QUMxZjdhYTIzZWM2YTdkNWM*************************",
      "Content-Type":"application/x-www-form-urlencoded"
    },
    payload: data,
    muteHttpExceptions: true
  };
  var response = UrlFetchApp.fetch(url, options);
}

function doPublish(mp3Name) {
  var url = "https://api.beebotte.com/v1/data/publish/test01/msg";
  var data = '{"data":"' + mp3Name + '"}';
  var options = {
    method: "POST",
    headers: {
      "X-Auth-Token":"token_****************",
      "Content-Type":"application/json"
    },
    payload: data,
    muteHttpExceptions: true
  };
  var response = UrlFetchApp.fetch(url, options);
}

function doSendMail(from, fromName, to, cc, subject, body) {
  GmailApp.sendEmail(
    to,
    subject,
    body,
    {
      from: from,
      name: fromName,
      cc: cc
    }
  );
}

現行の Google Home ではサポートされていない「能動的な発話」を擬似的に実現する google-home-notifier を利用しなければできないことであり、こういった使い方も同プログラムの実用的な応用例のひとつではないかと思います。さらに発展させることもできるでしょう。面白い時代になりました。



余談:「うるさいアラーム」機能を DIY した話

手元での google-home-notifier の利用例をもうひとつ紹介します。 Google Home のアラーム機能は手軽で何かと便利なので目覚まし用に使ったりしていました。ところがプリセットのアラーム音がいささか上品すぎるため手元では音量を最大にしても目覚めにつながらないケースが何度かありました。同じ経験をした方はおそらく少なくないのではないかと思います。

ちなみに、Google は 2018-02-01 のアップデートで Google アシスタントのアラーム音を変更可能としたようですが、この機能を利用できるのは現時点では言語設定が英語の場合のみで日本語環境では利用できません。

後日の対応に期待しつつ、前掲のエージェントを利用して耳障りでうるさいアラームを鳴らす機能を自作してみました。

まず、アラーム設定用・解除用の下のふたつの IFTTT アプレットを用意しました。所定のシートの A1 セルを使って音を鳴らす時刻の設定・解除を行う内容です。設定用のアプレットでは Google Assistant トリガーのオプション "Say a phrase with both a number and a text ingredient" での "a number" と "a text" を使って時・分のふたつの音声指示要素を受け入れています。
トリガー
アクション
トリガー
アクション

あわせて当該シートに以下の GAS コードを付与、スクリプト実行のトリガー「時間主導型」の「分タイマー」で「1分ごと」に doNotify() 関数の呼び出しを設定しました。シートに記録された時刻と現在の時刻が一致すると、Beebotte 上の "test01/msg" トピックへ "noise01.mp3" のキャスト再生を指示するメッセージが Publish されます。


// Beebotte へ MQTT メッセージを Publish
function doPublish(mp3Name) {
  var url = "https://api.beebotte.com/v1/data/publish/test01/msg";
  var data = '{"data": "' + mp3Name + '"}';
  var options = {
    method: "POST",
    headers: {
      "X-Auth-Token":"token_*************", 
      "Content-Type":"application/json"
    },
    payload: data,
    muteHttpExceptions: true
  };
  var response = UrlFetchApp.fetch(url, options);
}

function doNotify() {
  // シートをチェック。A1 に時刻が設定されていれば騒音を鳴らす
  var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadSheet.getSheets()[0];
  var val = sheet.getRange("A1").getValue();
  Logger.log(val);
  if (val == 0) {
    return;
  }
  var hm = val.split("_");
  Logger.log("set => " + hm[0] + ":" + hm[1]);  
  if (hm[0] >= 24 || hm[1] >= 60){
    //Logger.log("invalid");
    return;
  }
  var dt = new Date();
  var min = dt.getMinutes();
  var h = dt.getHours();
  if (String(h) == hm[0] && String(min) == hm[1]) {
    sheet.getRange("A1").setValue("0");
    doPublish("noise01");
  }
}

動作の様子: 動画 32秒

 

このように単機能でごくシンプルなものですが、手元では結構役に立っています。


(tanabe)
2018年06月29日

最近のPython-dev(2018-06)

はてなブックマークに登録

バックナンバー:

Python 3.7

日本時間の6/28に Python 3.7 がリリースされました。 終盤に駆け込みで2つ大き目の変更が入りました。

  • Unicode 11 対応
  • ASTの変更の revert

Unicode 11 はデータの更新だけなので危険が少ないし、1.5年後の3.8まちにはしたくないということでGoサインが出ました。

AST は、僕が中心で行ったAST段階での定数畳み込みの準備として、 docstring とそうでない文字列の区別をAST段階でつきやすくために変更していました。

その変更によりいくつかのライブラリで問題が報告されていたのですが、AST はもともと後方互換性を保証していないからとライブラリ側での対処がされていました。しかし、RCフェーズに入ってから IPython が他と異なる壊れ方をしていて、今のASTに関するAPIが IPython のニーズを満たしておらず想定外の対処が必要になっていたことから、一旦元に戻してまたAPIから考え直そうということになりました。

もちろんこのASTの変更を元に定数畳み込みが作られていたので、定数畳み込み後に docstring と文字列定数の区別がつかなくならないように Python 側にハックが必要になるのですが、僕が週末作業できないあいだに Serhiy がやってくれました。 https://github.com/python/cpython/pull/7121/files/8ee9227599583c460ff8faf9c1ab1670559a1224#r191080934

Compact GC Header

Python 3.8 に向けた大きめの改善の1つめに、2000年以来変更されてない循環参照GC用のメモリオーバーヘッドの削減に挑戦しています。

ちょうど 6/25 に、低レベルプログラミングが話題になることが多い Turing Complete FM のリスナーが集まる ミートアップイベンント があったので、そこで発表してきました。

基本的にはつぎのような戦略で、GC用ヘッダのメモリ使用量をポインタ3つ分から2つ分に削減しています。

  • 双方向リンクリストの逆方向リンクを、一時的に潰してGC用の参照カウントに使い、必要になる前に戻す。
  • ポインタの下位3bitを他の用途に使う (tagged pointer)。参照カウントに使うときは左に3ビットシフトする。

一旦動くようになったものの、問題がありました。

  • 参照カウントを3ビットシフトしたら、ポインタサイズが4バイトの32bitアーキテクチャでオーバーフローする危険がある。
  • マイナーなアーキテクチャ上のマイナーな malloc 実装が8バイトアラインしなかった場合、ポインタの下位3bitを使うのも危険。

その後、2種類の実装のメンテナンスはなるべく避けたいので、必要な期間が一番短かったビットを逆方向リンクのポインタではなく順方向リンクのポインタの下位ビットに押し込めるという改良を行いました。現在レビュー待ちです。

プルリクエスト

FASTCALL を PyFunction, PyCFunction 以外でも利用できるように

Python 3.6 から段階的に、インタプリタ内部で関数の引数の受け渡し方を変更する FASTCALL 方式が導入されています。

これを利用するために、現在は Python で実装された(バイトコードをインタプリタが実装する)関数を表すクラスと、C言語で実装された関数/メソッドを表すクラスが特別扱いされています。

さて、 Cython はもちろんC/C++言語で関数を実装するのですが、トレースバックなどでソースコードを表示できるようにしたいなど、Python組み込みのC言語で実装されたクラスでは足りない機能を追加するためには独自の型を使う必要があり、そうすると FASTCALL が利用できません。

それを改善するためにいくつかの提案がされていて、最新のものが PEP 580 になります。

提案者の議論の仕方がちょーっと強引すぎて摩擦を感じてはいるのですが、技術的にはまぁ正しい方向だと思うし、ちゃんと動く参照実装も素早く作ってくれているので、個人的には前向きに捉えています。

とはいえ、そもそも FASTCALL ってまだ破壊的変更が入るかもしれない内部専用APIを Cython が先走って採用しているだけで、どう考えても PEP 580 は先走りすぎなんですよね。

まだ 3.8 の開発は始まったばかりなので、 FASTCALL を stable & public 扱いにできないかの議論を始めたりしてフォローに回っています。

PEP 572 (代入演算子)

PEP 572 の議論は大炎上しました。5月にLanguage Summit があってそこで議論のオーバーヒートをどう扱うかなどの話がされたのですが、 LWN.net がその まとめ を作ってくれたところから、またML上での議論が再開し長大なスレッドになっています。。。


@methane

songofacandy at 14:39
この記事のURLComments(0)Python 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
Syndicate this site