Arduino MKR WAN 1310に搭載されたECC508を利用して乱数を生成してみる

Arduino MKR WAN 1310にはECC508というセキュアエレメントが搭載されており、これを用いて公開鍵認証などを安全に実現することができます。
ハードウェア乱数生成器を搭載しているため、暗号論的にセキュアな乱数を生成することもできます。

以前の記事で紹介したLoRa GPSノードでは公開鍵認証は利用しませんでしたが、共通鍵暗号のIVの初期化にこのチップを利用しているため、利用方法を書いておきます。

まず、ArduinoECCX08 ライブラリをライブラリマネージャで導入します。
f:id:NeoCat:20220412035243p:plain:w600

するとスケッチ例 > ArduinoECCX08 以下に幾つかのサンプルが追加されます。

このうちECCX08RandomNumberがまさに乱数生成をするサンプルなのですが、これを利用する前に lock という作業が必要です。これは設定をチップに書き込んで二度と改変できないようにする操作です。以下ではデフォルト設定 ECCX08_DEFAULT_TLS_CONFIG を利用するので、特定の用途で使う予定がある場合はそれを使う必要があるかもしれません。

スケッチ例の Tools > ECCX08CSR を書き込み、シリアルコンソールをつなぎます。

すると購入したばかりのMKRであれば、

The ECCX08 on your board is not locked, would you like to PERMANENTLY configure and lock it now? (y/N)

と聞かれますので、 y を入力すると、チップに初期設定が書き込まれます。

それ以降の操作はここでは不要です。

改めて ECCX08RandomNumber を書き込んでやると、乱数値がシリアル出力されるはずです。

最小では

// setup
ECCX08.begin();
if (!ECCX08.locked()) { error(...) }

long random = ECCX08.random(65535);

などと指定すれば乱数値が得られます。
なおlockせずに利用しようとするとRNGがテスト用に常に特定パターンを返すため、lockされていない場合はエラーにしてこの値を使用してはいけません。

他に以下のような乱数を取得するための関数が用意されています。

  long random(long max);
  long random(long min, long max);
  int random(byte data[], size_t length);

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の通信速度

続きを読む

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など定期的な維持処理が必要な機能は切っておく必要があります.

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程度の通電でうまく鳴りました

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

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全体を再起動するという処理を入れると、うまく再接続されています。