LoRaの通信波形をSDRで見てみた

前の記事で作成したGPSノードの通信の波形を、SDR (Software Defined Radio)で捉えて可視化してみました。
neocat.hatenablog.com


SDRの受信にはRTL2832Uなどのチップを積んだ安価なUSB TVチューナーなどが使えます。

が、今回は HackRF One を使用しました。
HackRF One + Portapack H2

受信ソフトウェアはMacで簡単に使える gqrx を使います。

まずインストールは Homebrew

brew install gqrx

でOK。アプリケーションフォルダに通常のアプリケーションとしてインストールされます。HackRF One を接続した状態で起動すると、デバイスにHackRF Oneが現れるので、選択して起動します。キャプチャのサンプルレートが選べますが、16000000 (16MHz) にしておきました。

起動したら、今回使用した周波数である923MHzを入力してチューニングします。下図ではピッタリに合わせていますが、このままだと真ん中に強いピークが出てしまうので、右のReceiver Optionsの方に1000 kHzくらいを入力してハードウェア周波数を922MHzなどにずらした方が良さそうです。

この状態で通信をすると、上図のようにFFTのところに赤いピークが現れます。が、このままだと荒すぎて波形は見れません。そこで、データを書き出して別のソフトで可視化を行なってみます。
メニューから Tools > I/Q recorder を開き、適当なディレクトリを指定して、通信の直前に Rec を押し、通信後に止めます。これで生データがファイル出力されます。数秒の Rec で数100MBから1GB程度のファイルが出力されるので注意してください。

可視化には inspectrum を使います。同じく brew でインストールし、コマンドラインから起動します。

brew install inspectrum
inspectrum

ウィンドウが開くので、 Sample rate を今回使用した 16000000 を入力し、 Open file で gqrx で出力したファイルを指定します。
すると、巨大な画像が表示されますので、横軸(秒)と周波数(今回だと 1000 kHz ずらして撮っているのでその付近)を参考に、スクロールしていくと、波形が見つかるはずです。Zoomや色付けするPowerを調整してやると、こんな感じでLoRaの通信波形が描画できました。

LoRaの通信波形

左側がGPSノードからのデータ送信、右側がゲートウェイからのACK送信です。(ゲートウェイの至近でキャプチャしたため、ゲートウェイ側の信号が強くて描画が荒れてしまっていますが…)

LoRa変調ではCSS(Chirp Spread Spectrum)、チャープスペクトラム拡散と呼ばれる方式を利用しています。使う帯域(今回は125kHz)分の周波数にわたって低い方から高い方に連続的に周波数をスイープさせているのが見てとれますね。これにより途中でノイズや受信できない周波数などがあっても影響を受けにくくなっています。
ノードからの送信では、最初に最低→最高までのスイープの繰り返しが10回あり、最高→最低に反転して2回繰り返しています。1つの繰り返しが1つのシンボルになります。ここまでの部分はプリアンプルで、受信側と同期を取るためにあります。
そこから先に少し複雑なパターンが現れますが、これはデータの数ビット分に応じて、シンボルのスイープの開始位置をずらすという符号化を行なっているものです。
例えば2bit、4通りの値を1シンボルで送るとすると、以下の4つのいずれかを送ることで 0, 1, 2, 3を表すことになります。
(Signals 2022より引用)

前の記事で SF によって送信速度/感度を調整できると書きましたが、この SF が1シンボルで送るbit数に相当します。SF=10であれば、1024通りのパターンを送るということになります。加えて、SFが1増えるごとに、1シンボルにかける時間を2倍にします。後者の方が通信時間に対する影響が大きいため、SFが1上がると概ねビットレートは半分ということになります*1。ゆっくり送信した場合の方が情報あたりの合計電力が上がりノイズ耐性が改善しますので、SFを上げることで通信距離が広げられるというわけですね。

ゲートウェイからの送信時は、スイープの方向が逆転しています。これはスケッチ中にあった

  LoRa.enableInvertIQ();                // active invert I and Q signals

という指定の効果で、ノードはゲートウェイからの通信のみを受信、ゲートウェイはノードからの通信のみを受信するために区別をつけるための機能です。

*1:例えばSF 9→10の場合、同一のデータ部分の送信にかかる時間は単純計算で 2*9/10 = 1.8倍 になります。プリアンプルなどデータの乗らない部分はそのまま2倍長くなるのでさらに2倍に近づきます。

GPSの現在位置をLoRaで送信して距離を測ってみた (Arduino MKR WAN 1310)

TL;DR

LoRa は最大数kmの長距離でセンサデータ等の通信ができるとされている通信方式です。
実際にGPSで取得した現在位置を送信するノードをArduino MKR WAN 1310を使って作成し、都市部(東京23区内)でこれを持ち歩いた時にどのくらいの距離まで通信できるのかを調べてみました。
通信距離はアンテナ設置状況で大きく変わるものです。今回はビルの窓にアンテナを貼り付けた程度の非常に簡易的なセットアップでしたが、それでも800m程度離れた場所でもデータを送信できることを確認できました。
f:id:NeoCat:20220409173351j:plain

LoRa / LoRaWANについて

LoRaは、LPWA(低消費電力・長距離)通信のための方式の1つです。日本では免許不要の920〜928MHz帯を利用して、バッテリーで長時間、センサデータ等の小さいデータを都市部で数km(開けた場所なら数十kmというデータも)の範囲で送信することができるとされています。
LoRaWANという単語の方が有名かもしれませんが、これはLoRaを利用して相互にデータを交換するためのプロトコル(L2)までを規定したものです。LoRaWANを利用した通信網を提供する商用サービスや、コミュニティで運営されているThe Things Network (TTN)もあります。他方、自分のデバイスゲートウェイ間で通信するだけであれば、LoRaWANを用いずに任意のプロトコルで通信することもでき、これはプライベートLoRaと呼ばれます。

参考解説記事: いまさら聞けないLoRaWAN入門:産業用ネットワーク技術解説(1/4 ページ) - MONOist


現実的に都市部でどれくらいの範囲で通信できるのかに興味があったため、GPSの現在位置をLoRaで送信するデバイスを作って試してみることにしました。
実験した場所は残念ながらTTNの圏外だったこともあり、今回はプライベートLoRaを利用しています。

LoRaの通信速度

続きを読む

USB Type-Cケーブルで接続すると動作しないQiワイヤレス充電器の謎を調べてみた

古めのUSB Type-C接続のデバイスの中には、なぜかUSB Type-C ⇔ Type-Cケーブルを使って電源に接続すると動作しないものがあります。


具体的には、リリース直後のM5StickCは、Type-C ⇔ Type-CケーブルでMacに接続すると動作しない問題がありました。
これは現在は改善されており、裏面のブロックダイアグラムに、「RTC」が記載されているロットでは正常に動作します。

他にも、2018年頃に買ったTronsmartのQiワイヤレス充電器(もう入手できない模様)はType-Cコネクタがついているのですが、同じくType-CコネクタのついたACアダプタにType-Cケーブルで接続すると電源が入りませんでした。

どちらも回避方法があり、USB Type-A ⇔ Type-C や Micro-B ケーブルにType-C変換アダプタをつけて接続してやるとなぜか動作します。

f:id:NeoCat:20220108182702j:plain f:id:NeoCat:20220108182714j:plain
USB Type-Cケーブル USB Type-A / Micro-Bケーブル + Type-Cアダプタ*2
動作しない 動作する

一体何が違うのか、USBケーブルチェッカーを使って調べてみました。
BitTradeOne ADUSBCIM USB CABLE CHECKER 2

USBケーブルチェッカーで調べてみた

まずはUSB Type-Cケーブルの方から。

一方USB Type-A ⇔ Micro-Bケーブルの両端にType-Cアダプタをつけてみた場合はこちら。

CCが接続されていることを示すLEDが消灯し、代わりにディスプレイにA側のCCが5.1kΩでプルダウンされ、B側のCCが56kΩでプルアップされていることが表示されています。
(CC1, CC2は上下の違いなので、上下逆に差し込めば逆になります。)

これは変換アダプタに内蔵された抵抗によるものなので、ケーブルなしでアダプタのみチェッカーに接続しても表示されます。

CCピンは、接続を検出したり、USB PD (Power Delivery)や映像伝送などで使われるAlt Modeへの切り替えをネゴシエーションするための通信線です。充電器や単純な機器の場合は抵抗でプルアップ・プルダウンすることで対応することもできます。

A側の5.1kΩプルダウンは、接続の向きを示すために入っているものです。USB Type-Cには上下それぞれにUSB 2.0のD+/D-信号線があるので、この抵抗を使って接続された向きを識別してどちら側を使うかが選択されます。


B側の56kΩプルアップは、給電能力を示すために入っています。表を見ると56kΩはUSB Default Powerで、この場合はUSB2.0接続なので0.5A(3.0なら0.9A)ということになります。


さて、これで違いはCCピンの処理にあることがわかりました。Type-Cケーブルで接続するとうまく動作しない機器は、電源やPCとCCピン同士が接続されてしまうと、通信によるネゴシエーションに失敗してしまってうまく接続できないのだろうと考えられます。アダプタを挟むとCCピン同士が接続されないので、失敗を回避できていたということです。

おまけで別のケーブルのチェック

Macの充電ケーブル。大電流が流せることを示すeMarkerが入ったUSB2.0ですね。

別のType-Cケーブルです。これも充電器は動作しない。USB3.0に対応しているのでTX/RX-1が配線されている。TX/RX-2やサブバンドは繋がっていないのでAlt Modeの映像出力やUSB3.1 Gen1x2などは利用できない。

Full Featuredなケーブルなら全部のLEDがつくはずだけど意外となかった…。

電源をUSB PDトリガー調べてみた

ついでなので、USB Type-C電源の方も、PD検出機能付きのチェッカーで調べてみました。

TC66C Type-C Bluetooth PDトリガーUSB電圧電流計容量計

TC66Cは双方向対応の電圧・電流チェッカーの機能のほか、安価ながらも、電源が対応しているプロトコルを検出したり、それを使って各種電圧を出力させるトリガー機能を持っています(QC, USB-PDの他, SamsungHuaweiのベンダ独自規格に対応。検出時はいろんな電圧が出力されてしまうので絶対に他のデバイスを接続しないようにしましょう)。

対応プロトコルと出力電圧・電流を調べたり(非対応は赤色)、

実際に選択した電圧を出力させたり(この写真だと20V出ていることが確認できます)、

QC3.0を使って0.2V単位で出力電圧を操作できたりします。


ところで、調べているうちに、このチェッカーを通して件のワイヤレス充電器を繋ぐと、どういうわけだかUSB Type-Cケーブルでもちゃんと電源が入って使えることが判明しました。あれれ? ただの電圧・電流チェックモードなので、何かのプロトコルをトリガーしている訳ではありません。それどころか、ちゃんとPDでネゴシエーションが成立して9Vを引き出せている模様*1

何かのインピーダンス整合だか電源ONタイミングだかの不具合が、このチェッカーのCCピン接続のおかげで解消したのでしょうか?
ということで、思わぬところでチェッカーが役に立つことが判明しました(笑)

*1:確かに裏面に9V入力対応と書いてあるけど、今までのアダプタ経由ではCCピンが繋がっていなかったので成功していなかった

M5Paperの動作中の消費電力を削減する

M5Paper を動作させっぱなしにすると残像が残る?

M5Paper は、静電容量タッチスクリーン付きの 540 x 960 の4.7インチ電子ペーパー(EPD)を備えた、ESP32搭載のデバイスです。
無線LANやBT通信などと組み合わせて、色々なモノを作ることができます。*1



1150 mAhのLipoバッテリーを持っているため、これと電子ペーパーの電源を落としても表示が消えない特性を生かして、長期間ディープスリープさせながら定期的に画像取得するような作例がよく見られます。
しかし、タッチセンサーを利用した動作をする場合、タッチ状態を監視して反応(画面を再描画したり通信したり)したいため、内部状態がリセットされてしまうディープスリープは使えず、USB電源等に繋いで常時稼働させる必要があります。


さて、M5Paperでタッチ操作が可能なスケジューラを作り、常時通電で稼働させていたのですが、時計を1分に1度更新していたところ、画面のUSB-Cポート側の端にだけ、白黒反転した残像のようなものが残るようになってきました。
※配線の都合上、上下反対(つまりUSB-Cポートの位置が上になるよう)にして使っています。
f:id:NeoCat:20210725112051j:plain:w400
最初は電子ペーパーの劣化かと思ったのですが、電源を落としてしばらく放置すると何事もなかったように元に戻ります。
そして、残像の残るあたりをタッチすると、なんだか暖かいことに気がつきました。
どうやらこの高温が残像の原因らしく、電源を落として冷ますと元に戻っていたようです。
温度を測ってみるとこんなかんじでした。ピンポイントで測ると40℃くらいになっています。。
f:id:NeoCat:20210725133040p:plain:w400

そういえば、ESP32は最大消費電力が大きいので発熱もそれなりにあり、常時稼働させていると気温センサ等で正しい値がとれなくなったりします。
というわけで、消費電力を抑えることを考えてみることにしました。

M5Paper稼働中の電流計測

まずは簡単にM5Paperの稼働中にどれくらいの電流が流れているか計測してみました。
フル充電後にUSB電源で動作させてUSBチェッカーで1秒ごとに計測した値なので、ピーク値ではなくラフな平均的な電流値です。

状態電流値
WiFi通信中180 - 200mA
WiFi待機中150 mA

WiFi待機中」は、WiFiアクセスポイントへの接続はしたまま通信はせずにloop()をぐるぐる回っている状態です。
普通のESP32と比べても大きい値となっています。
調べてみたところ、電子ペーパー用のコントローラ IT8951 (裏面にも描いてあります) が60-80msくらい電流を使っているようで、この分大きいということでした。

IT8951 の動作電流を抑える

EPD周りの電源は、まとめて以下の関数で落とすことができるようになっています。

M5.disableEPDPower()

しかしこれをやってしまうと、電源を再投入しても描画がノイズだらけになったり掠れたりしてしまい、リセットしない限り正常に復帰できないようです。
ディープスリープする場合は復帰時はどのみちリセットになるので構いませんが、タッチ操作に応答して画面を部分書き換えしたい場合にはリセットされては困ります。

IT8951 のデータシートを読んでみると、IT8951 には電源モードとして Active / StandBy / Sleep の3つがあり、StandBy / Sleep では一部の機能を抑止して消費電力を抑えられるとあります。
これを使うにはSPI等でコマンドを送る必要があるため、M5PaperのArduino用ライブラリに関数を足して、電源モードを切り替えられるようにしました。
Pull Request を送ってみました。)
これを使って待機中にはIT8951をSleepに入れてやり、再描画が必要になったらActiveに戻すようにしてやると、見事に60mA程度の電流をカットすることができました。
(StandByでも同じくらい電流が減ります。細かい違いは今回は測定できませんでした)

  M5.EPD.Sleep();
  // ... タッチ操作を待機する ...
  M5.EPD.Active();
  // ... タッチ操作に反応して再描画 ...

ESP32のライトスリープとの組み合わせ

残りの消費電力の削減はESP32のライトスリープによって行います。
まず、通信しない時はWiFiをOFFにしてしまいます。

  delay(1000); // 通信完了直後に OFF にしてしまうとコネクション切断処理が途中のまま残ってしまうため少し待機
  WiFi.mode(WIFI_OFF); // または   WiFi.disconnect(true);

再度通信する際は WiFi.mode(WIFI_STA); WiFi.begin(...) で再接続が必要なため、少し時間がかかります(固定IPアドレスにするといくらか時間を短縮できます)。


その上で、待機中に ESP32 をライトスリープに入れます。
ライトスリープなら、ディープスリープと違ってメモリ内容は維持され、復帰時もスリープに入ったところから処理が再開されるので、簡単に使えます*2

ライトスリープはタイマーやGPIO、UPCなど、様々な復帰要因に対応しています。
(→ 参考: ESP32のライトスリープを調べる | Lang-ship
今回は時計の画面更新のため、およびタッチスクリーン操作で復帰させたいので、以下のようにしました。

void loop() {
...
    Serial.flush();  // Serialをflushさせておく
    esp_sleep_enable_timer_wakeup(500000);  // 0.5s後に起床
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_36, LOW);  // タッチで GPIO36 が LOW になるのでこの時も起床
    esp_light_sleep_start();  // ライトスリープ開始

    // 起床後の処理
}

ほとんど delay() を入れるくらいの感覚で使えますね。

改善後の動作電流

さて、これらの対応でどのくらい動作電流は変わったか計測してみました。

状態電流値
WiFi通信中180 - 200mA
WiFi OFF時120 - 140mA
IT8951 Sleep時60mA
Light Sleep時? - 20mA

WiFi OFF + IT8951 Sleep + Light Sleep時には最低値で1桁mA程度まで減少させることができました(精度不足で細かい値は取れず)。
0.5s 毎の起床時と平均すると10mA前後といったところでしょうか。
元と比べると平均消費電力は10分の1程度に大きく節減でき、バッテリー動作でも1日以上は動作する計算になります。

発熱も大きく抑えられ、残像が出る問題も解決することができました。
f:id:NeoCat:20210725133105p:plain:w400


かなりの効果もあったことだし、Pull Request 取り込まれるといいなあ。

*1:v1.1が出ましたが、この記事はv1.0を元に書いています。基本的に同スペックなのでv1.1でも通用するでしょう。

*2:ただしメインCPUはpause状態となり処理が止まってしまうので、WiFiなど定期的な維持処理が必要な機能は切っておく必要があります.

Macのスリープが時々勝手に解除されるのを防ぐ

Macのスリープが時々勝手に解除されている?

前の記事でUSBファンを繋いだのですが、Macのスリープが数時間に1回、勝手に解除されてはまたスリープするという動作をしているらしく、時々ファン音がするのが気になるようになりました。ファン以外でも、USB HDDなどを繋いでいる場合も、勝手にスピンアップし始めて気になるというケースがままあるようです。

システム環境設定 > バッテリーで PowerNap を切ったりしたのですがそれだけでは改善せず。

この原因を調べてみました。なおOSバージョンはBig Surです。

調べ方は、 log show --last 10000 --style syslog | fgrep "Wake reason" で出てくる、以下のような行を見るというものです。調べてみると、さまざまな理由で起動してきている様子。

log show --last 10000 --style syslog | fgrep "Wake reason"
...
2021-07-11 07:51:29.506171+0900  localhost kernel[0]: (AppleACPIPlatform) AppleACPIPlatformPower Wake reason: EC.RTC (Alarm)
...
Wake reason = ARPT (Network)

無線LANを契機にスリープが解除されたときに出ます。
色々と対応方法はあるようですが、

sudo pmset -a tcpkeepalive 0

を実行してやるというのが有効なようです。(実行すると「スリープ中にFind My Macがうまく動かなくなるかもよ」と言ったメッセージが出ますが、どのみち移動してしまったらネットワーク接続できないので位置のレポートはできませんし、気にせず切ってしまいます。)

なお pmset は他にもスリープの動作に関して様々な設定ができるコマンドです。( 参考: man pmset , pmset で Mac の電源制御 - Qiita )

Wake reason = EC.RTC (Alarm)

タイマーで予約された起動です。
システム環境設定のバッテリー > スケジュールで予約されているもののほか、システムが勝手に予約していることもあります。
以下のファイルを見ると、どのプロセスがいつ起動を予約しているかを見ることができます。

/Library/Preferences/SystemConfiguration/com.apple.AutoWake.plist

今回の場合、「スクリーンタイム」が使用状況をレポートするためのエントリが2時間おきくらいに起動するよう設定しているのが見つかりました。
いちいちスリープ解除する必要なんてなさそうに思いますが、他の同一iCloudアカウントのデバイスとレポートを共有できるようなので、そのためなのでしょうか??
特に不要なので、システム環境設定 > スクリーンタイム > オプション で「オフにする」を実行します。
これだけではすぐに予約が解除されなかったため、上記のファイルを一度削除してから、バッテリー > スケジュール で予約を適当に ON→OFF と操作してファイルを再作成させ、予約が空になったことを確認します。

sudo rm /Library/Preferences/SystemConfiguration/com.apple.AutoWake.plist  # 一度削除


私の場合は上記で勝手に起動してくるのは収まって、静かになりました。バッテリーの持ち具合なんかにも影響しそうな気がします。

Macに外付けしたUSBファンの電源を自動でOn/Off

USBファンの電源を自動でOn/Off

夏になってきて暑くなってきました。そしてMacBook Proも負荷がかかるとCPU温度が上がり、サーマルスロットリングがかかって処理が遅くなる現象が起きるようになってきたため、外付けのUSB冷却ファンをつけてみました。


LiANGSTARノートパソコンスタンド 冷却ファンつき

確かにつけているとサーマルスロットリングは回避できるようになったのですが、手動でOnにするのをよく忘れるし、作業後にOffにするのも手間という課題が。
ちょっとした作業時は(多少とはいえ)音も気になるので止まっていて欲しいし、とはいえ熱くなってきたらスロットリングする前に稼働していて欲しい。

そんなわけで、熱やスリープ状態に応じてファンのOn/Offを自動化してみました。

制御に使ったのはUSBハブの、per-port power switchingというポートごとに電源をON/OFFできるという機能です。
これに対応しているものは結構少なく、情報もあまりありません。今のところ確実なのはSUGOI HUBという商品のようで、これはポート1, 2の2つみですが電源の制御が可能です。

システムトークス USB2-HUB4X-BK USB2.0ハブ

制御プログラム

制御に使うプログラムは以下です。Linux等でよく使われていますがlibusb(legacy)でMacでもちゃんと動作します。

hub-ctrl.c

以下のようにインストール。libusb-compatは brew で導入します。

$ brew install libusb-compat
$ curl -O http://www.gniibe.org/oitoite/ac-power-control-by-USB-hub/hub-ctrl.c
$ gcc -Os `pkg-config --cflags --libs libusb` hub-ctrl.c

これで、SUGOI HUBを接続して ./hub-ctrl を実行すると

Hub #0 at 020:064
 INFO: individual power switching.
Hub #1 at 020:052
 INFO: individual power switching.
Hub #2 at 000:024
 INFO: individual power switching.

などと表示されます。このうちどれがSUGOI HUBかは、アップルメニュー > このMacについて > システムレポート の USB を開き、製造元IDが「NEC Corporation」になっているハブを探して、その場所IDを見ると分かります。今回はこんな感じになっており、 場所IDの後ろの部分が 64 と 064 が一致しているので、 Hub #0 が該当するものと分かります。

あとは以下のように、ハブ番号 0 を -h オプションに、電源をON/OFFしたいポートを -P オプションに指定して、-p 1 で on, -p 0 で off にできます。

hub-ctrl -h 0 -P 1 -p 0  # ポート1の電源off
hub-ctrl -h 0 -P 1 -p 1  # ポート1の電源on

本体のファン回転数に連動させる

さて、このポート1にUSBファンを接続して、熱くなってきたら自動でONになるようにしてみます。
MacのCPU温度やファン回転数の情報は、以下のように powermetrics コマンドを実行すると5秒ごとに取得・レポートさせることができます。

$ sudo powermetrics --samplers smc
...
**** SMC sensors ****

CPU Thermal level: 94
GPU Thermal level: 44
IO Thermal level: 44
Fan: 5054.41 rpm
CPU die temperature: 88.95 C (fan)
GPU die temperature: 76.00 C
CPU Plimit: 0.00
GPU Plimit (Int): 0.00 
GPU2 Plimit (Ext1): 0.00 
Number of prochots: 0

なかなか熱々。CPU温度のところに出ている (fan) という表示はファンで温度制御しているよ、というような意味でしょうか。さらに熱くなってサーマルスロットリング中は (power) などと出てきて、CPU Plimitに0以上の数値が表示されます。冷えている時は何も出ません。

これを使って、ファンを制御してみましょう。いろんな方法が考えられますが、本体ファンに連動させてみることにしました。一定以上の回転数で回っていたらonにするようにrubyでプログラムを書いてみました。on/offに幅が設けてあるのは閾値付近で頻繁にon/offが切り替わるのを避けるためです。

#!/usr/bin/ruby

`sudo true`  # パスワード入力を促すため
io = IO.popen("sudo powermetrics --samplers smc", "r")
while (line = io.gets)
  print line
  if line.match(/Fan: ([\d.]+)/)
    fan = $1.to_f
    if fan > 4200  # ON閾値
      system('hub-ctrl -h 0 -P 1 -p 1')
    elsif fan < 3800  # OFF閾値
      system('hub-ctrl -h 0 -P 1 -p 0')
    end
  end
end

これを実行しておくと、そろそろ熱くなってきたぞ、という頃合いに、自動的にUSBファンが周り始め、かなりの場合サーマルスロットリングを避けることができるようになりました(流石に全コアずっと100%で回り続けているような状況では放熱が追いつかないこともありますが、それでも性能劣化の度合いは緩和されているようです)。
音も本体ファンが高速に回っているような状況ならどのみち気にならないし、なかなか便利に使えています。

M5PaperでSPIFFSに日本語TTFファイルを置いて描画する

M5Paperでスケジューラを作りました。
M5PaperのM5EPDライブラリは、日本語等のUTF-8の文字列をTrueTypeフォント(TTF)を使って描画することができますので、スケジューラでもこれを使っています。
最初はSDカードに日本語TTFを置いていたのですが、高々数MBのためにSDカードを専用に差しっぱなしするのも微妙だったので、SPIFFSに置くようにしてみました。以下、その方法です。

SPIFFS に入るフォントを用意する

SPIFFSは最大で7MBなので、これ以下のフォントを用意する必要があります。ギリギリ過ぎるとメタデータ分も必要なので収まりきりません。
私は比較的自由にダウンロード・利用できるIPAexゴシック(4MB程度)を使ってみました。

IPAexフォントおよびIPAフォントについて | 一般社団法人 文字情報技術促進協議会

microSDカードにフォントをコピーする

FAT32で初期化したマイクロSDカードに、font.ttf という名前でフォントを格納し、M5Paperに挿入しておきます。

SPIFFSにデータをコピーしてM5EPDに読み込ませる (Arduino IDE)

Arduino IDEから以下のコードを書き込みます。
(Arduino IDEでの開発環境は公式ページの導入手順に従ってあらかじめセットアップしておきましょう。)

この時、Arduino IDEのツールメニューにて、ボードとしてM5Stack-Fireを選択した上で、同メニューから Partition Scheme を「Large SPIFFS (7MB)」にしておくことが必要です。

setup()でtryCopySDFile()を呼び出すことで、microSDカードが認識されfont.ttfが存在するかをチェックし、その場合はSPIFFSを初期化して、そこにfont.ttfをコピーさせます。
microSDカードが見つからない場合は、何もしません。
その後、SPIFFSからフォントをM5EPDに読み込ませます。


正しくコピーできていれば、文字が描画できるはずです。
以下では試しにRTCの日付・時刻を描画しています。


うまくいったら、SDカードを抜きます。

#include <M5EPD.h>
#include <FS.h>

M5EPD_Canvas canvas(&M5.EPD);

// SDカードがセットされていれば、SPIFFSを初期化後font.ttfをコピーする
bool tryCopySDFile() {
  const char *path = "/font.ttf";
  if (!SD.begin(4) || !SD.exists(path)) {
    return SPIFFS.begin(false) && SPIFFS.exists(path);
  }

  canvas.drawString("Copying font ...", 270, 664);
  canvas.pushCanvas(0,0,UPDATE_MODE_DU4);

  Serial.println("Formatting SPIFFS ...");
  if (!SPIFFS.format()) {
      Serial.println("SPIFFS format Failed");
      return false;
  }
  if (!SPIFFS.begin()) {
      Serial.println("SPIFFS Mount Failed");
      return false;
  }
  Serial.println("Copying font ...");
  File file = SD.open(path);
  SPIFFS.remove(path);
  File dest = SPIFFS.open(path, FILE_WRITE);
  if (!file || !dest) {
    Serial.println("failed.");
    return false;
  }
  uint8_t *buf = new uint8_t[4096];
  if (!buf) {
    Serial.println("failed.");
    return false;
  }
  size_t len, size, ret;
  size = len = file.size();
  while (len) {
    size_t s = len;
    if (s > 4096)
      s = 4096;
    file.read(buf, s);
    if ((ret = dest.write(buf, s)) < s) {
      Serial.print("write failed: ");
      Serial.print(ret);
      Serial.print(" - ");
      Serial.println(s);
      return false;
    }
    len -= s;
    Serial.print(size - len);
    Serial.print(" / ");
    Serial.println(size);
  }
  delete[] buf;
  file.close();
  dest.close();

  if (!SPIFFS.exists(path)) {
    Serial.println("no file");
    return false;
  }
  dest = SPIFFS.open(path);
  len = dest.size();
  dest.close();
  if (len != size) {
    Serial.print("size not match : ");
    Serial.println(dest.size());
    return false;
  }
  Serial.println("Done.");
  return true;
}

void setup() {
  M5.begin();
  M5.TP.SetRotation(90);
  M5.EPD.SetRotation(90);
  M5.EPD.Clear(true);
  M5.RTC.begin();

  canvas.createCanvas(540, 960);
  canvas.setTextDatum(TC_DATUM);
  canvas.setTextSize(3);

  canvas.drawString("... Initializing ...", 270, 640);
  canvas.pushCanvas(0,0,UPDATE_MODE_DU4);

  if (!tryCopySDFile()) {
    canvas.drawString("data copy failed !", 270, 254);
    canvas.pushCanvas(0,0,UPDATE_MODE_DU4);
    for (;;);
  }

  // SPIFFSからフォントを読み込む
  canvas.drawString("Loading font ...", 270, 230);
  canvas.pushCanvas(0,0,UPDATE_MODE_DU4);
  canvas.loadFont("/font.ttf", SPIFFS);
  canvas.createRender(96, 256);
  canvas.createRender(32, 256);

  canvas.drawString("OK!", 270, 254);
  canvas.pushCanvas(0,0,UPDATE_MODE_DU4);

  // 時計を描画
  drawClock();
  M5.EPD.Clear(true);
}

void loop() {
  drawClock();
}


// 毎分時計を再描画する. RTCが設定されている必要がある
void drawClock() {
  char t[32];
  static int last_m = -1;
  rtc_time_t rtc_time;
  rtc_date_t rtc_date;
  M5.RTC.getTime(&rtc_time);
  M5.RTC.getDate(&rtc_date);
  if (last_m == rtc_time.min) {
    delay(1000);
    return;
  }
  last_m = rtc_time.min;

  const char* weeks[] = {"日", "月", "火", "水", "木", "金", "土"};

  canvas.setTextDatum(TL_DATUM);
  canvas.fillCanvas(0);
  canvas.drawLine(0, 179, 540, 179, 15);
  canvas.drawLine(0, 180, 540, 180, 15);

  snprintf(t, 32, "%d/%d/%d (%s)", rtc_date.year, rtc_date.mon, rtc_date.day, weeks[rtc_date.week]);
  canvas.setTextSize(32);
  canvas.drawString(t, 12, 16);

  snprintf(t, 32, "% 2d:%02d", rtc_time.hour, rtc_time.min);
  canvas.setTextSize(96);
  canvas.drawString(t, 12, 64);
  canvas.pushCanvas(0,0,UPDATE_MODE_GLR16);
}
続きを読む