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:もうどこにも在庫がなくて入手できなくなっているような…

*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);
}
続きを読む

Wireless Analog Terminal Bellを作った

Wireless Analog Terminal Bell

Analog Terminal Bell の動画にInspireされて、Wireless Analog Terminal Bell を作りました。

ターミナルで作業している時に何か間違えると、分かりやすく(物理的な)ベルが鳴ってお知らせしてくれます。

youtu.be

仕組み

端末へ出力されるのをPTYを中継するrubyプログラムが監視しており、ベル文字 "\a" ( "\x07" )が出てくるとHTTPリクエストを送信します。
これを受けて、M5Stick-C (バッテリー入りESP32として使っています) がソレノイドを動かし、ベルのハンマーを叩くことで音が鳴ります。

作り方

使ったもの

卓上ベル
小型ソレノイド 5V ROB-11015
MOSFET IRLU3410PBF
M5StickC

他にダイオードと適当な抵抗(1k〜10kΩくらい?)、安定動作のための大きめの電解コンデンサが必要です。

配線

こんな感じで配線します。(見にくいですが何となく察してください)

   M5Stick-C       FET  Diode   
 +-- BATT ---------------+-- Solenoid
 |   G26  -- 1kΩ -- G    ⊼     |
470uF               D ---+-----+
 +-- GND  --------- S

ソレノイドをうまくハンマーを押し出せる位置に、両面テープで固定します。
M5Stick-Cと回路はハンマーの邪魔にならないよう、ベルの台座の下にうまく納めます。

通電は一瞬*1とは言え、ソレノイドは結構な電流を消費するので、M5Stick-Cは十分充電しておく必要があります。

コード

使用したコードは以下にあります。

Wireless Analog Terminal Bell using M5Stick-C · GitHub

SSID等を設定してM5Stick-C にプログラムを書き込み、シリアルモニタでIPアドレスを確認します。
そして、pty-proxy.rb にそのアドレスを書き込み、Terminal上で起動します。

あとはベル文字を printf \\a 等で出力すれば、ベルが鳴ってくれるはず!

pty-proxy.rb について

pty-proxy.rb はPTYへの出力を拾ってそこからBELL文字を見つけたら削除した上でHTTPリクエストを送ります。
しかし、BELL文字だからとなんでも削除して良いわけではなく、エスケープシーケンスの一部を削除してしまうと無駄にベルがなったり端末の表示が崩れるかもしれません。
特に、OSC Mode というウィンドウタイトルなどを制御するエスケープシーケンスは終端に BELL文字を使っています。
そこで、エスケープシーケンス中かどうかを判定し、そうでない場合のみベルを鳴らすようにしています。

*1:私は50ms程度の通電でうまく鳴りました

Bluetooth対応のHue(スマートライト)をRubyとBLEで制御してみた

Bluetooth対応のHue(スマートライト)をRubyとBLEで制御してみた

去年くらいに、PhilipsからBluetooth LEに対応したHue (スマート電球) が発売されました。
HueはもともとZigbeeを使用しており、色や明るさの制御にはHueブリッジという装置をLANに接続する必要があったのですが、Bluetooth LEに対応したことでブリッジがなくともスマートフォンの専用アプリから操作することができるようになり、導入がしやすくなったということです。


Philips Hue フルカラー シングルランプ Bluetooth + Zigbee対応|E26 LED電球 スマートライト


これを、スマートフォンアプリを介さずにRaspberry Pi等から制御できないか試してみました。

結論から言えば操作は可能でした。

ただし、古いバージョンのbluezだとペアリングがうまくいかなかったり、スマートフォンアプリからファクトリーリセットを行わないとペアリングができなかったりするなど、ハマりどころが結構多く、やや面倒だなという感じです。
また、PhilipsはBLEでの制御インターフェースは正式サポートしているわけではないので今後アップデートで変わる可能性もあるかも、としているようで、素直にブリッジを導入してそのHue APIを使うのが無難ではあります。

やり方

先に、制御する側のPCやRaspberry Pi等に最新版のbluezを導入し、Bluetooth LEが使えるようにしておいてください。
5.50-1.2~deb10u1 というバージョンでは動作を確認できましたが、これより古い物では動作しませんでした。

sudo apt upgrade bluez
sudo systemctl start bluetooth
sudo hcitool lescan 

を実行して、ずらずらとBLEデバイスが検出されればOKです。うまくいかない場合、

sudo hciconfig hci0 down
sudo hciconfig hci0 up

BluetoothアダプタをOFF/ONしてみるとうまく動作することがあります。


制御用のスクリプトRuby gemにしてあるので、まずはgemをinstallします。(gemのコードはこちらです → https://github.com/NeoCat/ruby_hue_ble )
まず適当なディレクトリに Gemfile を作って以下のように記述します。
ble gemに依存していますが、これが割と古くて現在のbluezのインターフェースと変更があるので、パッチを当てたバージョンを指定する必要があります。(Merge Requet取り込んでくれないかなあ)

source 'https://rubygems.org'

gem 'hue_ble'

# ruby_ble-1.0.0 is not compatible with recent bluez DBus interface, so use patched version...
gem 'ble', git: 'https://gitlab.com/NeoCat/ruby-ble.git'

そしてgemをインストールします。

gem install bundler # bundler が入っていない場合
bundle install --path vendor/bundle

bundle exec irb を起動して試すのが簡単です。
まずライブラリを require し、Hueデバイスをスキャンします。
スキャンの前に、以下のペアリングが可能な条件を満たすようにする必要があります。

  • スマートフォンアプリ "Phillips Hue Bluetooth" を使って電球を登録したあと、「リセット」を実行する
  • リセットから時間が経っている場合、電球の電源をOFF/ONし直す
  • RasPi等のBLEアダプターと電球を十分に(<90cm)接近させる

ペアリングは一台のホストとしかできませんので、 "Hue Bluetooth"アプリとRasPi両方から制御するといったことはできません。

> require 'hue_ble'
=> true
> HueBLE::scan_cli
Scanning devices ...
Found 1 Hue devices:
  C1:23:45:67:89:AB  Hue Lamp
Found a new Hue Lamp  E2:34:56:78:9A:BC : Do you want to pair? (Y/n)> y

ペアリング済みのHue電球の一覧が(あれば)表示されます。
新規に見つかったHue電球がある場合、ペアリングするか聞いてきます。
なお、このアドレスはファクトリーリセットするとランダムに変わります。

ここで y を選択するとペアリングを試み、上記の条件が満たされていれば成功するはずです。

うまくいかない場合、

sudo hciconfig hci0 down
sudo hciconfig hci0 up

で改善されることもあります。
(しかし systemctl restart bluetooth するとすでにペアリングしていたデバイスが消えてしまいます…。こうなるとまた電球をスマートフォンアプリからリセットしてやり直すしかありません。
リセット時は HueBLE::scan_cli(true) とすると、全デバイスがbluezから一旦削除されるので、これでやり直しができます。)


スキャンが成功すると、 HueBLE クラス内にHue電球の一覧が登録され、 HueBLE.hues でハッシュとして取得できるようになります。(keys = BLEアドレス、values = デバイス制御用のオブジェクト)

登録されたら、

HueBLE.hues.each_value { |hue| hue.on }
HueBLE.hues.each_value { |hue| hue.off }

で全部まとめてON/OFFしたり、色(色温度またはフルカラー電球の場合は色)や明るさを取得したり制御したりできます。

> hue = HueBLE.hues['C1:23:45:67:89:AB']
> hue.brightness  # 明るさ: 1 - 254
254
> hue.brightness = 200
> hue.color_temperature = 100  # 色温度: 1 (白) - 511 (黄)
> hue.color = [16000, 1]  # 色: x, y. それぞれ 1 - 65534 , 範囲外だとエラーとなる。この例は紫色

色は、CIE XYZのx, yを、1〜65534までのいずれかの値に変換した数値で指定するようです。色空間の範囲外だとエラーが返ってきます。
CIE 1931 色空間 - Wikipedia

party!

# 全電球をカラフルにランダム変化させる
def party!
  30.times do
    HueBLE.hues.each_value do |hue|
      hue.brightness = rand(253) + 1
      hue.color_temperature = rand(510) + 1
      10.times do
        hue.color = [rand(65533) + 1, rand(65533) + 1]
        break
      rescue BLE::Characteristic::NotFound
        break
      rescue
        next
      end
    end
    sleep 1
  end
end

party!


とかやって楽しみましょう。

ESP32 (M5Stick-C) で電波時計を合わせよう

ふと気がついたら自宅の電波時計がかなり進んでいました。そこで、ESP32 (M5Stick-C)を使って擬似的に微弱なJJYの標準電波を発信するデバイスを作りました。

M5StickC


基本的には下記のコードをベースにしたものですが、ESP32のArduinoライブラリはNTP同期機能を含んでいるので、より簡単になっています。

neocat.hatenablog.com


Arduino IDEでESP32を使用するためのセットアップ手順は、下記サイトなどを参考にしてください。
esp32_setup – スイッチサイエンス


さて、JJYは40kHzまたは60kHzの信号ですが、これはLEDCをGPIOにアタッチし、 ledcWriteTone() で出力できます。

#define GPIO_PIN 10             /* JJY擬似信号を出力するピン. 10は内蔵の赤色LED */
#define LEDC_BASE_FREQ 60000.0  /* JJY擬似信号の周波数 */
#define LEDC_CHANNEL_0 0

  // 出力ON
  pinMode(GPIO_PIN, OUTPUT);
  ledcAttachPin(GPIO_PIN, LEDC_CHANNEL_0);
  ledcWriteTone(LEDC_CHANNEL_0, LEDC_BASE_FREQ);

  // 出力OFF
  ledcDetachPin(GPIO_PIN);
  pinMode(GPIO_PIN, INPUT);  /* ハイインピーダンスにする場合 */

簡単ですね。あとは、JJYの信号パターンに従って、ON/OFFさせればOK。
完成版のコードは本記事の末尾にあります。

ハードウェアとしては上記記事のArduino同様、指定したピンに適当な電線をつなぎ、適当な抵抗と、おまけに出力が分かりやすいようLEDでもつければ完成です。
が、私の電波時計の場合、なんと内蔵LEDを点滅させるだけでも、M5Stick-Cと電波時計を密着させれば受信できてしまいました…。バッテリーでも動作するし、なんともお手軽。

f:id:NeoCat:20200724072513j:plain


M5Stick-C自体にも日時を表示しています。これだけですでにNTP時計になっていますが,まあ。。