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時計になっていますが,まあ。。

Wio TerminalでBad Apple!!の影絵を再生してみた

Wio Terminalが届いたので、例の影絵を再生してみました。


Wio TerminalでBad Apple

ひとまず描画データはPCからUSB UARTで送信しており、音声は別再生です。
白黒2値限定ですが、差分だけを描画するという方法で、一部の前フレームとの差分が極端に大きいシーンを除いて320px × 240pxで30fps出ることが確認できました。
(とはいえほぼこの影絵専用なのであんまり応用が効かないという難点が…)

Get Started with Wio Terminal - Seeedウィキ(日本語版) に従って、Arduinoで開発していますが、
Wio Terminal側の再生用のソースコード(↓)はわずか32行で、非常に手軽に開発ができるようになっているのが良いですね。

#include"TFT_eSPI.h"
TFT_eSPI tft;

void setup() {
  tft.begin();
  tft.setRotation(1);
  digitalWrite(LCD_BACKLIGHT, HIGH);
  tft.fillScreen(TFT_BLACK);
}

uint16_t read_word() {  // シリアルからwordデータ受信
  while (!Serial.available());
  uint16_t x = Serial.read();
  while (!Serial.available());
  return (x << 8) | (uint16_t)Serial.read();
}

void loop() {
  static uint32_t col = TFT_WHITE;  // 白黒交互に描画する
  uint32_t offset = 0;
  int cnt = read_word();  // 差分描画データ数
  while (cnt--) {
    uint32_t start = offset + read_word();  // 差分描画開始位置
    if (start == 0xffff) {
      offset = 0xffff;
      start = offset + read_word();
    }
    uint16_t len = read_word();  // 連続して描画するピクセル数
    uint32_t finish = start + len;
    for (uint32_t j = start; j < finish; j = j - j % 320 + 320) {
      tft.drawFastHLine(j % 320, j / 320, finish - j, col);  // drawFastHLineで横1ライン分を描画
    }
  }

  col = col == TFT_WHITE ? TFT_BLACK : TFT_WHITE;
}


なおデータは以下のように作っています。

続きを読む

Arduino ESP32からBluetoothシリアル(SPP)デバイスに接続する

Arduino ESP32は、Bluetoothシリアル(SPP)のスレーブデバイスになるサンプルは付属しており、PCからはBluetoothシリアルで容易に接続することができました。
(例えば arduino-esp32/SerialToSerialBT.ino at master · espressif/arduino-esp32 · GitHub など。)

少し前に、Bluetoothシリアルのマスター側になり、他のデバイスに接続するためのライブラリやサンプルも用意されたようです。
arduino-esp32/SerialToSerialBTM.ino at master · espressif/arduino-esp32 · GitHub

これを使って、ESP32からBluetooth対応のUSB電圧/電流センサーに接続して情報を取得してみました。使ったUSBセンサーはこれ。

UM25C

arduino-esp32 は、Arduino IDEのボードマネージャから1.0.4にアップデートしたものを使っています。

ESP32からデバイスBluetooth SPPで接続する

まずは、setupでデバイスに接続を行ってみます。
接続は、Bluetoothバイスの名前、もしくはアドレスを直接指定して行います。
アドレスの方が高速に接続できるようです。

#include "BluetoothSerial.h"

BluetoothSerial SerialBT;

String name = "UM25C";
uint8_t address[6]  = {0x00, 0x11, 0x55, 0x33, 0x22, 0x77};  // アドレス直接指定も可能
char *pin = "1234"; //<- 接続用PIN。標準以外の場合は指定が必要

void setup() {
  Serial.begin(115200);

  // 初期化。名前とtrueを引数として渡すとマスターになる
  SerialBT.begin("ESP32test", true); 

  Serial.println(String("Connecting to ") + name + " ...");
  // 標準以外のPINが必要な場合はここで指定する
  // SerialBT.setPin(pin);

  // 名前を指定して接続する場合。スキャンのためか、10〜15秒程度かかる
  bool connected = SerialBT.connect(name);
  // アドレスを指定して接続する場合。こちらは2〜3秒で高速に接続できる
  //bool connected = SerialBT.connect(address);
  
  if (connected) {
    Serial.println("Connected Succesfully!");
  } else {
    while (!SerialBT.connected(10000)) {
      Serial.println("Failed to connect. retry ...");
    }
  }
}

Bluetooth SPPデバイスと通信する

このUSBセンサーは、0xf0 という 1 byte のコマンドを送ると、130バイトほどのバイナリデータを返してくるようになっています。
1秒に1回 ESP32 からコマンドを送り、受け取ったデータをシリアルでPCに返してみます。

byte buffer[130];
int pos = 0;

void loop() {
  static unsigned long last_wr = 0;
  if (millis() - last_wr > 1000) {
    SerialBT.write(0xf0);
    last_wr = millis();
  }
  while (SerialBT.available()) {
    buffer[pos++] = SerialBT.read();
    if (pos >= sizeof(buffer)) {
      handle_data();
      pos = 0;
    }
  }
  delay(20);
}

void handle_data() {
  String data = 
    String("{\"V\":") + String((buffer[2] * 256 + buffer[3]) / 1000.0, 3) +
    ",\"A\":" + String((buffer[4] * 256 + buffer[5]) / 10000.0, 4) +
    ",\"W\":" + String((buffer[8] * 256 + buffer[9]) / 1000.0, 3) +
    ",\"C\":" + String(buffer[10] * 256 + buffer[11]) +
    ",\"mAh\":" + String(buffer[16] * 65536L + buffer[17] * 4096L + buffer[18] * 256 + buffer[19]) +
    ",\"mWh\":" + String(buffer[20] * 65536L + buffer[21] * 4096L + buffer[22] * 256 + buffer[23]) +
    ",\"ohm\":" + String((buffer[124] * 256 + buffer[125]) / 10.0, 1) +
    "}\n";
  Serial.print(data);
}

これで、シリアルモニターにUSBセンサーの値がJSONっぽい形式で返ってきました。

{"V":4.697,"A":1.2048,"W":5.658,"C":19,"mAh":646,"mWh":3049,"ohm":3.8}
{"V":4.700,"A":1.3035,"W":6.126,"C":19,"mAh":647,"mWh":3051,"ohm":3.6}

現状の問題点

どういうわけか、デバイスと接続できなくても20〜30秒程度経過すると connected = true が返ってきてしまいました。
また、途中でデバイス側のリセットなどで切断された場合、仮に SerialBT.disconnect(); SerialBT.connect(); を実行し直しても再接続がうまくいきません。
仕方なく、一定時間応答がない場合、 ESP.restart() でESP32全体を再起動するという処理を入れると、うまく再接続されています。