デジタルマイクMP34DT05データのPDHによるFFT解析

1. 目的

Arduino Nano 33 BLE Senseには、デジタルマイクロフォンMP34DT05が搭載されており、16kHzサンプリングで16bitの符号付整数(32,768~-32,768)として出力されています。この音声データをBLE(Bluetooth Low Energy)を使って、PDHに送信し、PDHでFFTを行うシステムの動作確認を行います。

2.システム構成

システム構成を以下に示します。

2.1. 概要

今回のシステムは、デジタルマイクロフォン MP34DT05のサンプリング周波数はfs=16kHzを維持し、FFT結果の帯域を8kHz(ナイキストのサンプリング定理からfs/2)とします。そして、後段での処理の拡張性を残すため、FFTはPDH側で行います。
次にシステムブロック図を示します。

BLEでArduino Nano 33 BLE SenseからPDHに安定してデータを送信するのに、転送間隔を50ms程度開ける必要があることが分かっています(Appendix1参照)。そのため、1024個のデータを使ったFFTを約26秒間隔で行います。これはこのシステムの限界となりますが許容します。
1024個のデータを使った際の周波数分解能は、8kHz÷512=15.625Hzです。

PDH側では、前段のpythonプログラムがBLEで受け取ったデータをJSON形式に変換してNode-REDに送ります。Node-REDは、受け取ったデータを1024個に集約して後段のFFT用pythonプログラムに送ります。 FFT結果のデータと画像は、タイムスタンプを付けたファイル名のファイルとして保存されます(データはJSON形式、画像がpng形式)。PDH側のプログラム間のデータ転送はMQTTを使っています。Brokerは、Node-RED上にBrokerノードを配置しています。

Node-REDからは、BLE用の前段pythonプログラムとFFT用の後段pythonプログラムを起動/停止できます。

Node-REDのフロー画面を次に表示します。このフロー画面でシステムの制御と監視ができます。
大きく三つの部分に分かれます。上段がpythonのプログラムを起動する部分です。
中段は、前段のBLE通信用pythonのプログラムから送られてきた音声データを1024nまとめて、後段のFFT用pythonプログラムに渡す部分です。
下段は、FFT結果の画像を表示する部分です。また、下段の右側が、後段のFFT用pythonプログラムに渡すデータをデバッグ画面に表示させています。

2.2.Arduino Nano 33 BLE Sense

■ 開発環境
Arduino IDEでプログラミングします。

■ ライブラリ
BLEのライブラリは、ArduinoBLEを使用します。
また、MP34DT05のライブラリは、PDMを使用します。
Arduiono Nano 33 BLE Senseのボード、ライブラリ設定に関しては、4-2. 搭載センサの動作確認と4-3. 搭載センサとPDHBLE通信をご確認ください。

2.3. PDH

■開発環境
Node-RED, Python3を使用します。詳細は、4-3. 搭載センサとPDHBLE通信をご覧ください。

FFTは、python3のライブラリnumpyを使って行います。またライブラリmatplotlibを結果の描画に使用します。描画結果は、ファイルに保存されます。Node-REDでファイルの保存をチェックし追加された画像をフロー画面に表示します。

2.4. FFTの実行パラメータ

FFTの実行に使用するパラメータを以下にまとめます。

項目

サンプリング周波数

16,000Hz

サンプル数

1,024個

窓関数

ハニング窓

周波数分解能

15.625Hz

2.5. 窓関数の影響確認

窓関数として有名なのは、1)ハニング窓(hanning window)、2) ハミング窓(hamming window)、3) ブラックマン窓(blackman window)です。それぞれ窓関数の波形は下のようになります。ブラックマン窓が一番急峻で、ハミング窓が一番緩い形状をしています。ハミング窓は両裾が0レベルまで落ちていません。

窓関数の影響を調べるために、実際にFFTした結果を調べます。
入力信号として、700Hzと2kHzのsin波を合成した信号を使い、fs=16kHz、サンプリング数=1,024個でFFTを実施しました。


結果を次に示します。

1) ハニング窓

2) ハミング窓

3) ブラックマン窓

3.MP34DT05とPDHのBLE通信

ArduinoからPDHへの転送はBLEの特性(Characteristics)のNotifyを使って行います。MP34DT05の出力は、short型で16bitの符号付きの整数です。そこで、 short型の16bitを2つまとめて、long型の32bitとして送り、受信側で再度分割する方法で転送効率と転送成功率を上げます(Appendix 1参照)。
そのために、 BLEのライブラリ“ArduinoBLE”の“BLELongCharacteristic”を使って送信を行います。

3.1.データ送信時の頭出しの工夫

BLEでMP34DT05の16bitのデータを2つまとめて都度送るという動作をさせます。その際に、送られてきたデータの区切りをどうするうかという課題があります。そのために、先頭の32bitに“7FFFFFFF”を追加して送ります。PDH側では、送られてきたデータが”7FFFFFFF”であるかどうかを逐次調べます。みつかればそれが先頭の32bitということでそこから513(=1024/2+1)のデータを1つの送信データ列として扱います。先頭の32bitは捨てて残りの512個のデータ(512×32bit=1024×16bit)を使ってFFTを行います。

この制御のためにグローバル変数cntを使っています。フローを以下に示します。
cnt=0ならば、sending=7FFFFFFFHを送ります。cnt≠0ならば、MP34DT05の16bitデータ2つを合わせて32bitにして送信します。cnt=513になったら、cnt=0に初期化します。従って、513個の32bitデータを送ることになります。

3.2.Arduiono Nano 33 BLE senseのプログラム

1)概要
ライブラリとしてArduinoBLEとPDMを使用しています。
タブを2つ使用しています。メインタブ”peri-ble-pdm02“とサブタブ”update_MP34DT05”です。
プログラムはAppendix2に載せていますので、そちらをご覧ください。

2) プログラムの説明
以下、プログラムの簡単な説明をします。

a)メインタブ“peri-ble-pdm02”

関数定義は,setup()とloop()のみです。

■冒頭部

 ・ライブラリ読込み
 ・グローバル変数の設定とUUIDの定義
 ・サービスとcharacteristicのクラスの初期化

以下の様に、サービスと特性(Characteristics)はそれぞれ1つ定義しています。

ここでは、特性(Characteristics)として、ArduinoBLEの“BLELongCharacteristic”を使用しています。特性としては“BLERead | BLENotify”を設定しています。

■ setup部

setup{

 ・シリアル通信設定

 ・内蔵LED設定
 ・PD24DT05(PDM)スタート
 ・BLEスタート+LocalName, DeviceName設定
 ・MP34DT05初期化:関数“set_MP34DT05()”の実行
 ・アドバタイズ開始

}

1) シリアル通信、内蔵LED、MP34DT05の初期設定を行います。
2) set_MP34DT05();で、サービスSensor_MP34DT05_Serviceのアドバタイズの準備を行います。
3) BLE.advertise();でアドバタイズをスタートします。

loop部

loop(){
 ・BLE接続要求確認

  ・セントラル機器から接続要求がある場合{
   ・セントラル機器に接続
   ・内蔵LED点灯
   ・セントラル機器に接続されている間、以下を繰り返し実行{
     ・関数”Read_PDM_Data()“を実行
     ・関数”notify_PDM()“を実行
     ・関数“counter()”を実行
     ・50ms待つ
   }
   ・内蔵LED消灯
   ・セントラル機器と切断
 }
 ・接続要求が無い場合
   ・関数”readMP34DT05_nonBLE();を実行
}

1)セントラル機器(Central Device)からの接続要求が来るのを待ちます。
接続要求が無い間、関数readMP34DT05_nonBLE()を実行し、内蔵の3色LEDをMP34DT05のデータのレベルに合わせて点灯します。

2)接続要求が来たら、接続処理が完了するまで以下の処理を繰返します。
  ①内蔵LEDを点灯します。
  ②セントラル機器と接続が完了したか確認する。

3)セントラル機器と接続が完了したら、接続が切れるまで以下の処理を繰返します。
  ①Read_PDM_Data()を実行し、MP34DT05からデータを取得します。
  ②notify_PDM()を実行し、notifyでデータを送信します。
  ③counter()を実行し、転送したデータの数をカウントします。
  ②50ms待ちます : delay(50)

4)セントラル機器と接続が終了したら、内蔵LEDを消灯します。

■サブタブ”update_MP34DT05
サブタブには、メインタブから呼び出される関数が記述されています。
onPDMdata(), set_MP34DT05(), Read_PDM_Data(), IncDataGen(), counter(), notify_PDM(), readMP34DT05_nonBLE(), LED_Control()の8つの関数が定義されています。

■ onPDMdata()

1)PDM(MP34DT05)に読出しデータ可能データがあるかをチェックします。返値として読み出し可能Byte数が返ってきますのでbytesAvailableにセットします。
2)読出し可能Byte数を読み出し、sampleBuffer[]に格納します。
3)sampesReadに読み出したデータの数をsampleReadにセットします。データ1個は16bitなので、bytesAvailableの半分です。

■ set_MP34DT05()
1)AdvertisedServiceとして、Sensor_MP34DT05_Serviceを設定し、
2)そのサービスの特性として、MP34DT05_PDMを設定します。
3) サービスとして、 Sensor_MP34DT05_Serviceを設定します。

■ Read_PDM_Data()

PDMのデータをsendData[]に読み出します。
1)samplesReadが256になるのを待ちます。
2)sampleBuffer[]のデータをsendData[]にコピーします。
3)上記コピーをMAX_SAMPLES/256(=1024/256=4)回繰返します。
コピーするsendData[]のアドレスは都度ずらします。
4)最後にsamplesRead=0に設定します。

■IncDataGen()

送信動作確認用に送信データを0~MAX_SAMPLESのインクリメントデータにしたいときに呼び出す関数です。
sendData[MAX_SAPMLES]に0 ~MAX_SAMPLESのインクリメントデータをセットします。
通常は使用しません。テスト時のみ使用します。

■ counter()

nofity_PDM()で送信する回数を制御するためのカウンタです。
PDMのデータが16bitに対し、nofity_PDM()で1回に送るデータが32bitなので、MAX_SAMPES/2+1(1024/2+1 = 513)回送ったら、Read_PDM_Data()を呼びだすという動作のために変数cntを使っています。
1)呼び出されるたびに、変数cntに1を足します。
2)cnt>=MAX_SAMPLES/2 + 1になったら、cnt = 0 に初期化します。

■notify_PDM()

MP34DT05の16bitデータを32bitのデータに加工して送信します。
1)cnt=0なら、sendingに0x7FFFFFFFをセットします。
2)cnt≠0なら、i=2×(cnt – 1)を計算します。そして、
sending=((long)sendData[i+1] << 16) | (sendData[i] & 0xFFFF);
で、2つの16bitデータを32bitデータに加工します。
3)sendingをBLEで送ります。

■ readMP34DT05_nonBLE()

1)MP34DT05のデータを読み出します。
2)samplesReadの数だけ、sampleBuffer[i]の値をsampleDataにセットし、LED_control()を読み出します。

■ LED_Control()

Arduino Nano 33 BLE Senseに内蔵されている3色LEDを音の大きさに応じて点灯させます。
1)sampleDataの値に応じて8段階でRed、Green、BlueのLEDを点灯させます。
2)digitalWrite(LEDR, LOW)で赤色が点灯します。
同様にdigitalWrite(LEDG, LOW)で緑色が、 digitalWrite(LEDB, LOW)で青色が点灯します。

3.3. PDHのBLEプログラム(python)

ライブラリBleakを使って、BLE受信のPythonのプログラムを作成します。
このプログラムは大きく3つのことを行います。
 1) BLEでNotifyデータの受信
   bleakライブラリとasyncioライブラリを使って、Notifyに対応した非同期処理を行います。
 2) 受信した結果をjson形式に変えます。
   jsonライブラリを使って、受信データを以下の様なjson形式にします。
             sdata = {“sensor”:”MP34DT05″,”func”:”PDM”,”time”:1703477983.7988958,”data”:[-12,-12]}
 3) paho.mqtt.clientのライブラリを使って、json形式に変換した受信データをLocalhost内のmqttブローカーにパブリッシングします。

3.3.1.必要なライブラリのインストール

BLE用のライブラリとmqtt用のライブラリを端末画面からインストールします。

用途

ライブラリ名

インストールコマンド

BLE用ライブラリ

bleak

python3 –m pip install bleak

mqtt用ライブラリ

paho-mqtt

python3 –m pip install paho-mqtt

3.3.2.プログラムの概要説明

プログラムは、Appendix.3に掲載しています。参照ください。

■ 定義部

 ・ ライブラリのimport


import sys
import signal
import asyncio
from bleak import BleakClient

import paho.mqtt.client as mqtt
import time
import json
import struct

 ・ペリフェラル機器のMACアドレス設定
 ・UUIDの設定
 ・mqtt ブローカーの設定
 ・mqttクライアントを作成
 ・json形式用の辞書型データ準備

 ペリフェラル機器のMACアドレスとUUIDのスキャンの仕方は、R-CPS-HP 第6巻 5-1. BLE機器との接続を参照ください。

■関数定義部

 ・publish(sdata)
   jsonデータを文字列に変換して、パブリッシングします。
 ・bytes_to_log(data)
   送られてきたデータを4バイトずつ読込み、long型(32bit)に変換した後に、上位16bitと下位16bitに分けて、符号つき16bit整数に変換後、配列Data[2]に収めて返します。
 ・notification_handler9(sender, data: bytearray)
   UUIDにNotificationが来た場合の処理内容です。bytes_to_long(data)を呼び出して、符号つき16bit整数に変換後、JSONデータにに格納した後にpublish(sdata)でtopic=“pdm_pic”にパブリッシュします。
 ・main(address)
   ・mqttブローカーへの接続
   ・asyncioを使って、ペリフェラルへ接続します。
   ・Notifyの受信処理を開始します。
   ・while True:
     await asyncio.sleep(1.0)
     で、1秒間のスリープを無限に繰り返します。
   ・キーボード割り込み(Ctrl-C)で、notifyの停止を行います。
   ・ mqttを切断します。

実行部

 ・asynio.run()
   ・mainを実行します。その際に、引数が1つあれば、mainの変数である“ADDRESS”に渡します。

3.3.3.プログラムの動作説明

3.3.3.1. BLEの受信動作

asyncioライブラリを使った非同期処理を行います。

async with BleakClient(address) as client:
    print(f"Connected: {client.is_connected}")
    await client.start_notify(char_uuid9, notification_handler9)
    try:
        while True:
            await asyncio.sleep(1.0)
    except KeyboardInterrupt:
        print("Stop!! Key board Interrupt!!");
        await client.stop_notify(char_uuid9)
    finally:
        await client.stop_notify(char_uuid9)

1行目で、MAC addressを指定して接続要求を出します。接続がなされると、2行目で、“Connected: アドレス”の表示を行います。
3行目で、特性(Characteristic)のUUIDを指定してNotify処理の開始をリクエストして、待ち状態に入ります。指定したUUIDのNotifyデータが来ると、関数notification_handler9が呼びだされます。その際に、受信したbytearrayデータが渡されます。
5,6行目は、notifyの待ち状態を無限に続けるための記述です。1秒間スリープする動作を“while True:”で無限に繰り返します。
4行目と7~11行目は、無限ループを止めるために、“Ctrl-C”によるキーボード割り込みをキャッチするループです。キーボード割り込みをキャッチすると、notify動作を停止します。

notification_handler9の動作

def notification_handler9(sender, data: bytearray):
"""Simple notification handler which sends MP34DT05 PDM data"""
    data1 = bytes_to_long(data)
    sdata['sensor'] = 'MP34DT05'
    sdata['func'] = 'PDM'
    sdata['time'] = time.time()
    sdata['data'] = data1
    publish(sdata)

notification_handler9は、呼び出される際にbytearray型のdataを渡されます。渡されたbytearray型dataは、bytes_to_log(data)に渡され、2つの符号付16bit整数に変換されてdata1に返されます。
data1を辞書型データに代入して、関数publish()に渡します。

bytes_to_long()の動作
関数bytes_to_longは、バイト列を受け取り、それを2つの符号付き16ビット整数に変換します。具体的な動作は以下の通りです:

def bytes_to_long(data):
    Data = [0]*2
    for i in range(0, len(data), 4):
        # 4バイトずつ読み込み、それをlong型に変換
        long_data = struct.unpack('I', data[i:i+4])[0]
        # 上位と下位の16bitに分ける(符号付き)
        Data[1] = struct.unpack('h', struct.pack('H', (long_data >> 16) & 0xFFFF))[0]
        Data[0] = struct.unpack('h', struct.pack('H', long_data & 0xFFFF))[0]
    return Data

Data = [0]*2:2つの要素を持つリストDataを初期化します。これらの要素は後で上位と下位の16ビットに分けた値を格納するために使用されます。
for i in range(0, len(data), 4)::入力されたバイト列を4バイトずつ処理します。
long_data = struct.unpack(‘I’, data[i:i+4])[0]:4バイトを取り出し、それを符号なし32ビット整数(unsigned long)として解釈します。
Data[1] = struct.unpack(‘h’, struct.pack(‘H’, (long_data >> 16) & 0xFFFF))[0]:long_dataの上位16ビットを取り出し、それを符号付き16ビット整数(short)として解釈し、Data[1]に格納します。
Data[0] = struct.unpack(‘h’, struct.pack(‘H’, long_data & 0xFFFF))[0]:long_dataの下位16ビットを取り出し、それを符号付き16ビット整数(short)として解釈し、Data[0]に格納します。
return Data:上位と下位の16ビットを格納したリストDataを返します。

3.3.3.2. MQTTのパブリッシング動作

MQTTのパブリッシング動作は大きく3つに分かれます。
 1) 設定
 2) 接続、切断
 3) パブリッシング

順次説明します。
1)設定

# MQTT ブローカーの設定
broker_address = "localhost"
port = 1883
topic = "pdm_pic"
# MQTT クライアントを作成
mqtt_client = mqtt.Client()

2行目で”localhost“をbroker_addressとしてます。PDH内のNode-REDにブローカーを立ち上げていますので、アドレスは、”localhost“になります。
3行目はポート番号です。標準の1883を使っています。ブローカーのポートに合わせる必要があります。
4行目で、topicを”pdm_pic”としています。
6行目で、mqttのClientを立ち上げてています。

2) 接続、切断

接続と切断は、main()関数の中で行っています。
a) 接続

# MQTT ブローカーに接続

 # MQTT ブローカーに接続
mqtt_client.connect(broker_address, port)
print(f"MQTT connected: {broker_address}")

b) 切断

 # MQTT クライアントを切断
mqtt_client.disconnect()
print(f"MQTT disconnected: {broker_address}")

3) パブリッシング

パブリッシングは、関数publish()で行っています。

def publish(sdata):
# Publish on MQTT
json_data = json.dumps(sdata)
mqtt_client.publish(topic, json_data)

引数として、辞書型データsdataを受取ります。4行目で辞書型データをjsonデータに変換します。6行目で、パブリッシングしています。

4.FFT前の準備(Node-RED)

FFT前の準備として、Node-REDのフローで以下の項目を実施します。

① 送られてきたデータから不要な情報を削除します。
② 日時データを追加します。
③ 送られてきたデータから先頭のデータを検出し、1024個のデータに集約します。
④ 後段のFFT用pythonプログラムにトピック名”fft_data”でパブリッシュします。
⑤1024個の音声データをcsvファイルとして保存します。
Node-REDのフローは、Appendix4に載せています。参照ください。

以下、簡単に説明します。

① 送られてきたデータから不要な情報を削除します。
changeノードを使って、以下の情報を削除しています。
  1) msg.payload.sensor
  2) msg.payllad.func
  3) msg.payload.time

② 日時データを追加します。
Date/Time Formatterノードを使って、時刻情報を追加します。フォーマットは、ファイル名にも使用できるように”2023-12-25_13_28_39”のようにしています。

③送られてきたデータから先頭のデータを検出し、1024個のデータに集約します。

functionノードを使って、javascriptのプログラムで以下の処理を行っています。
1) 先頭データ(0x7FFFFFFF)の検出
2) 先頭データの次のデータから1024個のデータを集める
3) msg.payload.dataとして次段にデータを送る。

以下に、functionノードの中のプログラムのフローを示します。実際には、変数のflow変数としていますが、ここでは煩雑になるのを避けるために、flowの記述を外しています。詳細は、この後に載せているコードをご覧ください。
countは送られてきたデータ数で、cycleは1024個データ揃った回数を示します。
初期化で、cycle、countを0に設定し、count_max=1024にします。また、配列arrayの中身を0に設定します。

・データが送られてくると「開始」からスタートします。
・data[1]が先頭データ“0x7FFF”だと、countを0に設定するとともに配列arrayの中身も0に設定して「終了」し、次のデータを待ちます。

・data[1]が先頭データ出ないときには配列array[count+1], array[count]にデータを入れて、count値を2つ進めます。
・そして、countがcount_maxに等しくなったら、
  msg.payload.data = array:
  msg.payload.cycle = cycle;
と代入し、cycleを1つ増やします。
・そして、msgを返して「終了」し、次のデータを待ちます。

④後段のFFT用pythonプログラムにトピック名”fft_data”でパブリッシュします。
mqtt_outノードでmsg.payloadをパブリッシュします。

⑤1024個の音声データをcsvファイルとして保存します。
csvノードとwrite fileノードを使って、データをcycle, count, datetimeとdataをファイルに追記します。

5. FFT計算処理プログラム(python)

後段のpythonプログラムは、Node-REDで集約された1024個のデータをFFTしてグラフ化もします。

 

FFTにはライブラリnumpyを使います。グラフ化にはライブラリmatplotlibを使います。また、データの受け取りには、paho.mqtt.clientライブラリを使用します。
そして、次の処理を行います。プログラムはAppendix5として載せてあります。参照ください。
1) mqttでtopic “fft_data”をsubscribeします。
2) subscribeしたデータで、FFTを実行します。
3) FFT結果をグラフ化し、データと共にファイルに保存します。

5.1. プログラムの概略説明

■ ライブラリの読込み部
上記のライブラリ以外にも、jsonと保存ファイル名に日時を使用するためにtimeを使用します。

import paho.mqtt.client as mqtt
import numpy as np
import matplotlib.pyplot as plt
import json
import time

■ 変数宣言部
1)mqttブローカー用変数
2)FFT用変数
3)窓関数宣言:窓関数は”hanning窓”を使用します。

■ 関数宣言部
1)on_connect関数:クライアントがMQTTブローカーに接続したときに呼び出されるコールバック関数を設定します。接続が成功したことを表示するとともに、変数topicに設定されている”fft_data”を購読(サブスクライブ)します。2)on_message関数:クライアントがMQTTブローカーからメッセージを受信したときに呼び出されるコールバック関数を設定します。FFTの処理を行います。
3)main関数:mqttの接続、切断の処理を行います。

■ 実行部
main関数を実行します。

5.2. プログラムの動作説明

プログラムの主要な部分の動作説明を行います。

MQTTの接続/切断処理

MQTTの接続/切断に関係する箇所を以下に抜き出しました。

# MQTT ブローカーの設定
broker_address = "localhost"
port = 1883
topic = "fft_data"

def main():
    # MQTT クライアントを作成
    mqtt_client = mqtt.Client()

    mqtt_client.on_connect = on_connect
    mqtt_client.on_message = on_message

    # MQTT ブローカーに接続
    mqtt_client.connect(broker_address, port)
    print(f"MQTT connected: {broker_address}")

    try:
        mqtt_client.loop_forever()
    except KeyboardInterrupt:
        # MQTT クライアントを切断
        mqtt_client.disconnect()
        print(f"MQTT disconnected: {broker_address}")

1)MQTTブローカーの設定

・ broker_address = “localhost”
  ブローカーアドレスは、”localhost”です。PDH内のNode-REDに配置しているブローカーに購読(サブスクライブ)します。
・ port = 1883
  ポートは標準の1883を使用します。
・ topic = “fft_data“
  トピックは”fft_data”

2) main()関数内の設定

・  mqtt_client = mqtt.Client()
  MQTTのクライアントを”mqtt_client“と言う名前で作成します。
 ・ mqtt_client.on_connect = on_connect
  クライアントがMQTTブローカーに接続したときに呼び出されるコールバック関数を”on_connect“という関数に設定します。
 ・ mqtt_client.on_message = on_message
  クライアントがMQTTブローカーからメッセージを受信したときに呼び出されるコールバック関数を”on_message”という関数に設定します。
・  mqtt_client.connect(broker_address, port)
  上記で設定したアドレスとポートのMQTTブローカーに接続します。
・  mqtt_client.loop_forever()
  クライアントとして接続をずっと続けます。
・ mqtt_client.disconnect()
  クライアントがブローカーとの接続を終了します。例外処理でキーボードからCtrl-Cが押されたときに終了し、ブローカーから切断します。

■ MQTTのサブスクライブ処理

MQTTのサブスクライブ処理に関する部分を以下に抜き出しました。

def on_message(client, userdata, msg):
dict_data = json.loads(msg.payload)
y = np.array(dict_data["data"])

・ def on_message(client, userdata, msg):
  上記の“mqtt_client.on_message = on_message”で設定したメッセージを受信した際に呼び出される関数on_message()を定義します。
・ dict_data = json.loads(msg.payload)
  受信したメッセージ(msg.payload)はバイト列で、これをjson.loads()関数でJSONオブジェクト(辞書)に変換します。
・ y = np.array(dict_data[“data”])
  JSONオブジェクトdict_dataの中のキー“data”をnumpy配列に変換し、変数yに代入します。このyが、Arduino Nano 33 BLE SenseのMP34DT05から送られてきた1024個の音声データです。以下、これを使って、FFT処理を行います。

■ FFT計算(1):FFT演算処理

FFTの演算に関する部分のプログラムを以下に抜き出しました。

# FFT Configulation
fs = 16000       # sampling frequency
N  = 1024       # number of samples
dt = 1/fs       # sampling period

# Window function
window = np.hanning(N)

    y = y * window

    # FFTを実行
    y_fft = np.fft.fft(y)               # 離散フーリエ変換
    freq = np.fft.fftfreq(N, d=dt)      # 周波数を割り当てる
    Amp = abs(y_fft/(N/2))              # 音の大きさ(振幅の大きさ)

    # 窓補正
    acf=1/(sum(window)/N)
    Amp = acf*Amp

・ fs = 16000       # sampling frequency
  FFTするデータのサンプリング周波数を設定します。ここでは、MP34DT05のサンプリング周波数16000Hz(=16kHz)に設定します。
・ N  = 1024       # number of samples
  FFTするデータの数を設定します。ここでは、1024を設定します。
・ dt = 1/fs       # sampling period
  FFTするデータのサンプリング周期を設定します。周期は周波数の逆数なので、dt=1/fsで設定します。
・ window = np.hanning(N)
  窓関数を設定します。numpyのライブラリに以下の5つの窓関数が収められています。ここでは、hanningを使用します。引数としてサンプル数Nを渡します。
   numpy.hanning, numpy.blackman, numpy.hamming, numpy.bartlett, numpy.kaiser
・ y = y * window
  窓関数をサンプリングデータにかけ合わせます。
・  y_fft = np.fft.fft(y)
  1次元の離散フーリエ変換を実施します。引数として、窓関数処理後の音声サンプリングデータを渡します。
・ freq = np.fft.fftfreq(N, d=dt)
  離散フーリエ変換のサンプル周波数を取得します。引数は、サンプリング数Nとサンプリングデータの周期dtを渡します。
・  Amp = abs(y_fft/(N/2))
  フーリエ変換の結果から振幅を計算します。音の大きさを求めます。
・ acf=1/(sum(window)/N)
  Amp = acf*Amp
  窓関数の補正を行います。窓関数を適用すると、信号のエネルギーが減少するため、その影響を補正します。

■ FFT計算(2):グラフ化

FFT結果のグラフ化に関する部分を以下に抜き出しました。


output_FN = "/home/pi/Documents/pdm_imu/pictures/"+dict_data["datetime"]+".png"

### 音波のスペクトル ###
plt.plot(freq[1:int(N/2)], Amp[1:int(N/2)]) # A-f グラフのプロット
plt.xlabel("frequency [Hz]")
plt.ylabel("amplitude [V/rtHz]")
plt.xscale("log")                           # 横軸を対数軸にセット
plt.yscale("log")                           # 縦軸を対数軸にセット
plt.savefig(output_FN, dpi=300)
plt.clf()

 ・ output_FN = “/home/pi/Documents/pdm_imu/pictures/”+dict_data[“datetime”]+”.png”

  結果のグラフを保存するファイルの保存先の設定。ファイル名の重複を避けるためにサブスクライブしたJSONデータのキー”datetime”を埋め込んだファイル名にしています。   

以下、matplotlib.pyplotのコマンドです。
・ plt.plot(freq[1:int(N/2)], Amp[1:int(N/2)])
  横軸を周波数、縦軸を振幅に設定します。FFT結果はナイキストの定理より1/2の周波数までしか使えないのとDC成分を削除するために、1:int(N/2)で範囲を制限しています。
・ plt.xlabel(“frequency [Hz]”)
  plt.ylabel(“amplitude [V/rtHz]”)
  横軸、縦軸のラベル指定です。
・ plt.xscale(“log”)
   plt.yscale(“log”)
  横軸、縦軸ともに対数軸に設定します。
・ plt.savefig(output_FN, dpi=300)
  画像をファイルに保存します。
・ plt.clf()
  プロット画像を消します。

■ FFT計算(3):データのファイル保存

FFT結果データのファイル保存に関する部分を以下に抜き出しました。

    # NumPy配列をPythonリストに変換
    freq_list = freq[1:int(N/2)].tolist()
    amp_list = Amp[1:int(N/2)].tolist()

    # PythonリストをJSON形式に変換
    out_data = {"Num":N, "datatime":dict_data["datetime"], "freq": freq_list, "amp": amp_list}
    data_json = json.dumps(out_data)

    outFile = "/home/pi/Documents/pdm_imu/fftResult/"+dict_data["datetime"]+".json"
    # JSONデータをファイルに出力
    with open(outFile, "w") as f:
        f.write(data_json)

・freq_list = freq[1:int(N/2)].tolist()
 amp_list = Amp[1:int(N/2)].tolist()
  周波数と振幅のデータのnumpy配列をpythonのリストに変換します。
・out_data = {“Num”:N, “datatime”:dict_data[“datetime”], “freq”: freq_list, “amp”: amp_list}
  pythonのリストに変換したFFT結果をJSONのオブジェクトにします。
・data_json = json.dumps(out_data)
  JSONオブジェクトデータをJSONフォーマットの文字データにダンプします。
・outFile = “/home/pi/Documents/pdm_imu/fftResult/”+dict_data[“datetime”]+”.json”
  出力ファイル名を設定します。ファイル名の重複を避けるためにdatetime情報を使用します。
・with open(outFile, “w”) as f:
     f.write(data_json)
  指定したファイルに保存します。

6. FFT結果表示のNode-REDフロー

Node-REDのフローからは、前段のBLE通信用pythonプログラムと後段のFFT用pythonプログラムを起動することができます。また、以下の様にFFT結果の画像もフロー画面に表示することができます。

pythonプログラムの起動はexecノードを使っています。詳細は、こちら(コマンドの実行や別プログラムの起動)をご覧ください。
ここでは、フォルダに保存されたファイルを監視し画像を表示するフローについて説明します。
フローはwatchノードとfunctionノードとviewerノードの3つで構成されています。

1)watchノードの動作
watchノードは、指定したディレクトリのファイルの変化を検知し、変化があったファイルのフルパス名をmsg.payloadに返してくれます。
以下のプロパティ設定では、FFT結果の画像ファイルの保存されているディレクトリを指定しています。

2) functionノードの動作
functionノードでは、前回変化が有ったとwatchノードから連絡があったファイル名と今回のファイル名が同じであったら、そのファイル名を後段に連絡します。
これは、一見するとおかしな動作に思われますが、pythonでファイルを保存する際にwatchノードの出力を確認すると以下の様になります。
 ① ファイルが作られた ⇒ 1回目のwatchノードの連絡
 ② ファイルサイズが変わった ⇒ 2回目のwatchノードの連絡
従って、ファイルが作られた際と書込みが完了した際の2回watchノードから、msg.payloadに乗ってファイル名が来ます。そこで、同じファイルが2回来たら書込みが完了したということで後段にファイル名を伝えます。

functionノードのコード欄に記載されているプログラムは以下の様になります。

if (context.get("fileName") === undefined) {
    context.set("fileName", "newfile.png")
}
if (context.fileName == msg.payload){
    context.fileName = msg.payload;
    return msg;
} else {
    context.fileName = msg.payload;
}

・1行~3行目は、初期化動作です。変数fileNameが無かったら、”newfile.png”という値をセットしたfileNameという変数を作ります。有れば何もしません。
・4行~9行目は、msg.payloadで送られてきたファイル名が前回と同じかどうかを調べています。
同じであれば、fileNameにmsg.payloadを保存して、msgを返します。異なれば、fileNameにmsg.payloadを保存するだけです。

3)viewerノードの動作
viewerノードは、指定されたファイルを自分の下に表示するノードです。”node-red-contrib-image-tools”に含まれています。
このノードは、”read-fileノード”と“imageーoutノード”を合わせた動作をするノードです。
プロパティ画面で、表示するファイル名もしくはファイル名が送られてくるmsg名を指定するだけです。ここでは、msg.payloadと指定しています。

Appendix.1 BLEでの音声データの転送間隔検討

1.目的

Arduino Nano 33 BLE Senseに搭載されたデジタルマイクロフォンMP34DT05の音声データをBLEでPDHに転送しFFTをかけるシステムを検討しています。
1回のFFTに必要な1024個のshort型(16bit)のデータをBLEでデータを転送する際に、PDH側でデータの取りこぼしが起こり、FFTの解析の頻度がかなり落ちることが分かりました。転送間隔を40ms, 50msとした場合の実験結果は以下の表1の様になりました。対策案を検討します。

        表1. BLE転送成功率

転送間隔

1024個の転送時間

転送成功率

40ms

41.0秒

62%(=179/287)

50ms

51.2秒

70%(=43/61)

2.転送方法の検討結果

MP34DT05の出力は、short型で16bitの符号付きの整数です。BLEのライブラリ“ArduinoBLE”の“BLEShortCharacteristic”を使って送信を行った結果が、表1の結果です。
そこで、 short型の16bitを2つまとめて、long型の32bitとして送り、受信側で再度分割する方法を検討します。この方法であれば、同じ1024個の転送時間でも転送回数が半分になるので転送間隔を伸ばすことができ、転送成功率を上げられます。
残念ながら、Arduino Nanoは、double型でも32bitなので、2つのデータを一度に送るのが限界です。
BLEのライブラリ“ArduinoBLE”の“BLELongCharacteristic”を使って送ります。
この方法で、BLE転送成功率を取得したのが、表2です。
50ms以上であれば、100%受信できることが確認できました。さらにFFTの間隔は、当初予定の30秒を達成できることも分かりました。

         表2. BLE転送成功率

転送間隔

1024個の転送時間

転送成功率

30ms

15.4秒

76%(=195/256)

40ms

20.5秒

99%(=197/198)

50ms

25.6秒

100%(=158/158)

60ms

30.7秒

100%(=141/141)

Appendix.2 MP34DT05データBLE転送プログラム

Arduino Nano 33 BLE Senseに搭載されているMP34DT05の音声データをBLEで転送するプログラムを下記に載せます。
参考にしてください。2つのタブから構成されています。
メインタブ:peri-ble-pdm02.ino
サブタブ :update_MP34DT05.ino

1) peri-ble-pdm02.ino

/*
 * Arduino LSM9DS1 : send sensordata on BLE
 * Arduino MP34DT05 : Digital Microphone
 */
 
#include 
#include 

#define localNAME "peri_pdm"
#define DeviceNAME "PDM"

// UUID for MP34DT05(Digital MicroPhone)
#define MP34DT05_SERVICE_UUID "9997c274-9b20-4181-808f-ef2de31032a8"
#define MP34DT05_PDM_Characteristic_UUID "9997c274-9b21-4181-808f-ef2de31032a8"

// send data size
#define MAX_SAMPLES 1024
short sendData[MAX_SAMPLES];

//Variables for MP34DT05
short PDM_MP34DT05 = 0;
short sampleBuffer[256];
short sampleData;
short sampleDataBuffer[256];
unsigned long theTime;
volatile int samplesRead;
int cnt = 0;

// BLE Service
BLEService Sensor_MP34DT05_Service(MP34DT05_SERVICE_UUID);

// BLE Characteristic
BLELongCharacteristic MP34DT05_PDM(MP34DT05_PDM_Characteristic_UUID, BLERead | BLENotify);

void setup() {
  Serial.begin(115200);
  //while (!Serial);
  Serial.println("Started");

  // initialize the built-in LED pin to indicate when a central is connected
  pinMode(LED_BUILTIN, OUTPUT); 
  
  // MP34DT05 begin initialization
  PDM.onReceive(onPDMdata);

  // optionally set the gain, defaults to 20
  // PDM.setGain(30);

  // initialize PDM with: one channel (mono mode) & 16 kHz sample rate
  if (!PDM.begin(1, 16000)) {
    Serial.println("Failed to start PDM!");
    while (1);
  }

  // BLE begin initialization
  if (!BLE.begin()) {
    Serial.println("starting BLE failed!");
    while (1);
  }
  BLE.setLocalName(localNAME);
  BLE.setDeviceName(DeviceNAME);

  // Initialize MP34DT05 service
  set_MP34DT05();

  // start advertising
  BLE.advertise();
  Serial.println("Bluetooth device active, waiting for connections...");
}

void loop() {
  // wait for a BLE central
  BLEDevice central = BLE.central();
 
  // if a central is connected to the peripheral:
  if (central) {
    Serial.print("Connected to central: ");
    // print the central's BT address:
    Serial.println(central.address());
    // turn on the LED to indicate the connection:
    digitalWrite(LED_BUILTIN, HIGH);
 
    // while the central is connected:
    while (central.connected()) {
      if (cnt == 0){
        Read_PDM_Data();   // reads 1024 data from MP34DT05
        //IncDataGen();      // Incremental data generation for sending test
      }
      notify_PDM();       // SEND_BLE
      counter();
      delay(50);
    }

    // when the central disconnects
    digitalWrite(LED_BUILTIN, LOW);
    Serial.print("Disconnected from central: ");
    Serial.println(central.address());
  } else {
      readMP34DT05_nonBLE();    
  }
}

2) update_MP34DT05.ino

#include 
#include 

void onPDMdata() {
  // query the number of bytes available
  int bytesAvailable = PDM.available();

  // read into the sample buffer
  PDM.read(sampleBuffer, bytesAvailable);

  // 16-bit, 2 bytes per sample
  samplesRead = bytesAvailable / 2;
}

void set_MP34DT05(){
  // add the service UUID
  BLE.setAdvertisedService(Sensor_MP34DT05_Service);

  // add characteristic
  Sensor_MP34DT05_Service.addCharacteristic(MP34DT05_PDM);

  // Add service
  BLE.addService(Sensor_MP34DT05_Service);
}

// read pdm data and set to the dimensions
void Read_PDM_Data(){
  // Wait for samples to be read
  for (int j = 0; j<(MAX_SAMPLES/256); j++){
    while (samplesRead!=256);
    for (int i = 0; i < samplesRead; i++) {
      sendData[j*256+i] = sampleBuffer[i];
    }
    // Clear the read count
    samplesRead = 0;
  }
}

// Increment data generator for sending test
void IncDataGen(){
  for (int i=0; i<MAX_SAMPLES; i++){
    sendData[i] = i;
  }
}

// counter for send data control
void counter(){
  cnt++;
  if (cnt >= (MAX_SAMPLES/2+1)) cnt = 0;
}

void notify_PDM(){
  int i = 2*(cnt - 1);
  long sending;
  short sendingL, sendingH;
  if (cnt == 0){
    sending=0x7FFFFFFF;
  } else {
    sending=((long)sendData[i+1] << 16) | (sendData[i] & 0xFFFF);
  }
  MP34DT05_PDM.writeValue(sending);
  sendingH = (short)((sending >> 16) & 0xFFFF);
  sendingL = (short)(sending & 0xFFFF);
  Serial.print(cnt);
  Serial.print(": "); 
  Serial.print(sendingH);
  Serial.print(", "); 
  Serial.println(sendingL);
}

void readMP34DT05_nonBLE() {
  // print samples to the serial monitor or plotter
  for (int i = 0; i < samplesRead; i++) {
    sampleData = sampleBuffer[i];
    LED_Control();
  }
}

void LED_Control(){
  if (abs(sampleData)>=0 && abs(sampleData) < 50){
    digitalWrite(LEDR,HIGH);
    digitalWrite(LEDG,HIGH);
    digitalWrite(LEDB,HIGH);
  } else if (abs(sampleData) < 100){
    digitalWrite(LEDR,HIGH);
    digitalWrite(LEDG,HIGH);
    digitalWrite(LEDB,LOW);        
  } else if (abs(sampleData) < 150){
    digitalWrite(LEDR,HIGH);
    digitalWrite(LEDG,LOW);
    digitalWrite(LEDB,HIGH);        
  } else if (abs(sampleData) < 200){
    digitalWrite(LEDR,HIGH);
    digitalWrite(LEDG,LOW);
    digitalWrite(LEDB,LOW);        
  } else if (abs(sampleData) < 250){
    digitalWrite(LEDR,LOW);
    digitalWrite(LEDG,HIGH);
    digitalWrite(LEDB,HIGH);        
  } else if (abs(sampleData) < 300){
    digitalWrite(LEDR,LOW);
    digitalWrite(LEDG,HIGH);
    digitalWrite(LEDB,LOW);        
  } else if (abs(sampleData) < 350){
    digitalWrite(LEDR,LOW);
    digitalWrite(LEDG,LOW);
    digitalWrite(LEDB,HIGH);
  } else {
    digitalWrite(LEDR,LOW);
    digitalWrite(LEDG,LOW);
    digitalWrite(LEDB,LOW);
  }
}

Appendix.3 PDHのBLEプログラム(python)

本文中で紹介しているPDHのBLEブログラムです。python 3.9.2で動作しています。
”ble_pdh04.py”という名称です。Node-REDのexecノードでこの名称で実行していますので、ご注意ください。

# -*- coding: utf-8 -*-
#####
# Successively receive micro-phone data of int type
# Rev.0.03: 2023/11/16 無限loopにする。
# Rev.0.04: 2023/11/20 short X 2 = Longの転送に対応
#####
import sys
import signal
import asyncio
from bleak import BleakClient

import paho.mqtt.client as mqtt
import time
import json
import struct

# setting for BLE
ADDRESS = (
    "33:07:90:D9:XX:XX" # Arduino Nano 33 BLE sense Mino-2
)

# UUID for MP34DT05
CHARACTERISTIC_UUID9 = "9997c274-9b21-4181-808f-ef2de31032a8" # read & notify

# MQTT ブローカーの設定
broker_address = "localhost"
port = 1883
topic = "pdm_pic"
# MQTT クライアントを作成
mqtt_client = mqtt.Client()

data = [0.0,0.0,0.0]
sdata = {    # 辞書型データ
    'sensor': 'MP34DT05',
    'func': 'none',
    'time': 'time',
    'data': data
}

def publish(sdata):
    # Publish on MQTT
    json_data = json.dumps(sdata)
    mqtt_client.publish(topic, json_data)

def bytes_to_long(data):
    Data = [0]*2
    for i in range(0, len(data), 4):
        # 4バイトずつ読み込み、それをlong型に変換
        long_data = struct.unpack('I', data[i:i+4])[0]
    # 上位と下位の16bitに分ける(符号付き)
    Data[1] = struct.unpack('h', struct.pack('H', (long_data >> 16) & 0xFFFF))[0]
    Data[0] = struct.unpack('h', struct.pack('H', long_data & 0xFFFF))[0]
    return Data

def bytes_to_double(data):
    return struct.unpack('d', data)[0]

def bytes_to_signed_int(data):
    return int.from_bytes(data, byteorder='little', signed=True)

def notification_handler9(sender, data: bytearray):
    """Simple notification handler which sends MP34DT05 PDM data"""
    data1 = bytes_to_long(data)
    sdata['sensor'] = 'MP34DT05'
    sdata['func'] = 'PDM'
    sdata['time'] = time.time()
    sdata['data'] = data1
    publish(sdata)

async def main(address):
    char_uuid9 = CHARACTERISTIC_UUID9

    # MQTT ブローカーに接続
    mqtt_client.connect(broker_address, port)
    print(f"MQTT connected: {broker_address}")

    print(address, char_uuid9)
    async with BleakClient(address) as client:
        print(f"Connected: {client.is_connected}")
        await client.start_notify(char_uuid9, notification_handler9)
        try:
            while True:
                await asyncio.sleep(1.0)
        except KeyboardInterrupt:
            print("Stop!! Key board Interrupt!!");
            await client.stop_notify(char_uuid9)
        finally:
            await client.stop_notify(char_uuid9)

    print("Disconnected")
    # MQTT クライアントを切断
    mqtt_client.disconnect()
    print(f"MQTT disconnected: {broker_address}")
        
if __name__ == "__main__":
    asyncio.run(
        main(
            sys.argv[1] if len(sys.argv) > 1 else ADDRESS,
        )
    )

Appendix.4 Node-REDのフローファイル

本文で紹介しているNode-REDのフローファイルです。
Node.js v18.16.0,  Node-RED v3.0.2で動作しています。
このフロー中には、MQTTのブローカーノードが入っていないので、同じPDH(Raspberry Pi)のフローの何処かにブローカーを配置して下さい。別の端末のブローカーを使う場合には、mqtt関係のIPアドレスを変更する必要があります。

ディレクトリ関係は環境に合わせて修正する必要があります。

[{"id":"b92c72884452607b","type":"tab","label":"PDM_PIC0","disabled":false,"info":"","env":[]},{"id":"eb667179704ae718","type":"change","z":"b92c72884452607b","name":"削除","rules":[{"t":"delete","p":"payload.sensor","pt":"msg"},{"t":"delete","p":"payload.func","pt":"msg"},{"t":"delete","p":"payload.time","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":230,"y":340,"wires":[["f2e0b41e61d59d98"]]},{"id":"840928bbe2304943","type":"mqtt in","z":"b92c72884452607b","name":"","topic":"pdm_pic","qos":"2","datatype":"auto-detect","broker":"496bc63a53e5165b","nl":false,"rap":true,"rh":0,"inputs":0,"x":100,"y":340,"wires":[["eb667179704ae718"]]},{"id":"6b39757a0019cb48","type":"debug","z":"b92c72884452607b","name":"debug 73","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":800,"y":480,"wires":[]},{"id":"fcddb3a469f1195c","type":"change","z":"b92c72884452607b","name":"filename","rules":[{"t":"set","p":"filename","pt":"msg","to":"filename","tot":"flow"},{"t":"set","p":"filename","pt":"msg","to":"filename&\".csv\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":680,"y":340,"wires":[["b86e08f6db1ccd43"]]},{"id":"b86e08f6db1ccd43","type":"csv","z":"b92c72884452607b","name":"","sep":",","hdrin":"","hdrout":"none","multi":"one","ret":"\\n","temp":"cycle,count,datetime,data","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":810,"y":340,"wires":[["5f704ff7e909bcf0"]]},{"id":"5f704ff7e909bcf0","type":"file","z":"b92c72884452607b","name":"","filename":"filename","filenameType":"msg","appendNewline":false,"createDir":true,"overwriteFile":"false","encoding":"none","x":940,"y":340,"wires":[[]]},{"id":"f2de7a511eddd62f","type":"comment","z":"b92c72884452607b","name":"送信データ集約","info":"","x":120,"y":220,"wires":[]},{"id":"c6d91d227d1d8218","type":"inject","z":"b92c72884452607b","name":"初期化","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":260,"wires":[["c02c5bc558f0cf77","4fb20a31deffaa5d"]]},{"id":"c02c5bc558f0cf77","type":"function","z":"b92c72884452607b","name":"初期化","func":"flow.set('count', 0);\nflow.set('cycle', 0);\nflow.set('count_max', 1024);","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":240,"wires":[[]]},{"id":"4fb20a31deffaa5d","type":"function","z":"b92c72884452607b","name":"配列作成","func":"// 配列の要素数\nflow.set(\"length\", 1024);\n// 配列作成\n//if (!context.flow.array) {\ncontext.flow.array = new Array(flow.get(\"length\"));\n//}\n// 配列の初期化:データを0で埋める\ncontext.flow.array.fill(0);","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":280,"wires":[[]]},{"id":"503549393a32743c","type":"inject","z":"b92c72884452607b","name":"受信開始","props":[{"p":"filename","v":"/home/pi/Documents/pdm_imu/receive_","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"93:B3:5F:16:CD:A7","payloadType":"str","x":140,"y":80,"wires":[["afa7ca985f556c5d","a7dd8a739a9e0401"]]},{"id":"afa7ca985f556c5d","type":"moment","z":"b92c72884452607b","name":"日時","topic":"","input":"","inputType":"date","inTz":"Asia/Tokyo","adjAmount":0,"adjType":"days","adjDir":"add","format":"YYYYMMDD_HHmmss","locale":"ja-JP","output":"datetime","outputType":"msg","outTz":"Asia/Tokyo","x":290,"y":120,"wires":[["d839b8ea10bbcadd"]]},{"id":"d839b8ea10bbcadd","type":"change","z":"b92c72884452607b","name":"filename","rules":[{"t":"set","p":"filename","pt":"flow","to":"filename&datetime&ext","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":120,"wires":[[]]},{"id":"a7dd8a739a9e0401","type":"exec","z":"b92c72884452607b","command":"python3 /home/pi/source/python/ble/ble_pdh04.py","addpay":"payload","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"ble_pdh04","x":310,"y":60,"wires":[[],["4b9e541a10ea3b4e"],["4b9e541a10ea3b4e"]]},{"id":"4b9e541a10ea3b4e","type":"debug","z":"b92c72884452607b","name":"debug 74","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":460,"y":60,"wires":[]},{"id":"d03001a5483a7983","type":"mqtt out","z":"b92c72884452607b","name":"","topic":"fft_data","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"496bc63a53e5165b","x":680,"y":380,"wires":[]},{"id":"80e071f702c8e792","type":"function","z":"b92c72884452607b","name":"png file","func":"if (context.get(\"fileName\") === undefined) {\n    context.set(\"fileName\", \"newfile.png\")\n}\nif (context.fileName == msg.payload){\n    context.fileName = msg.payload;\n    return msg;\n} else {\n    context.fileName = msg.payload;\n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":480,"wires":[["615fda249ef06afe","b296b5e594363430"]]},{"id":"fad8be86f73b7229","type":"watch","z":"b92c72884452607b","name":"png監視","files":"/home/pi/Documents/pdm_imu/pictures","recursive":"","x":140,"y":480,"wires":[["80e071f702c8e792"]]},{"id":"615fda249ef06afe","type":"image viewer","z":"b92c72884452607b","name":"","width":160,"data":"payload","dataType":"msg","active":true,"x":410,"y":480,"wires":[[]]},{"id":"b296b5e594363430","type":"debug","z":"b92c72884452607b","name":"debug 75","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":420,"y":440,"wires":[]},{"id":"2f636e0a129e1d4e","type":"mqtt in","z":"b92c72884452607b","name":"","topic":"fft_data","qos":"2","datatype":"auto-detect","broker":"496bc63a53e5165b","nl":false,"rap":true,"rh":0,"inputs":0,"x":670,"y":480,"wires":[["6b39757a0019cb48"]]},{"id":"f2e0b41e61d59d98","type":"moment","z":"b92c72884452607b","name":"日時","topic":"","input":"","inputType":"date","inTz":"Asia/Tokyo","adjAmount":0,"adjType":"days","adjDir":"add","format":"YYYY-MM-DD_HH_mm_ss","locale":"ja-JP","output":"payload.datetime","outputType":"msg","outTz":"Asia/Tokyo","x":350,"y":340,"wires":[["35acbef30630d644","e159219189a042dc"]]},{"id":"d2e98d8914ce0525","type":"inject","z":"b92c72884452607b","name":"FFT開始","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"/home/pi/source/python/ble/fft_calc02.py","payloadType":"str","x":640,"y":60,"wires":[["582cb6604b366206"]]},{"id":"582cb6604b366206","type":"exec","z":"b92c72884452607b","command":"python3 ","addpay":"payload","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"fft_calc02","x":780,"y":60,"wires":[[],["fd60ec46c1ef8d91"],["fd60ec46c1ef8d91"]]},{"id":"fd60ec46c1ef8d91","type":"debug","z":"b92c72884452607b","name":"debug 76","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":920,"y":60,"wires":[]},{"id":"c3aa19db00daae06","type":"inject","z":"b92c72884452607b","name":"FFT終了","props":[{"p":"kill","v":"","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":640,"y":100,"wires":[["582cb6604b366206"]]},{"id":"48c695c84ea80cb6","type":"inject","z":"b92c72884452607b","name":"受信終了","props":[{"p":"kill","v":"","vt":"str"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":140,"y":120,"wires":[["a7dd8a739a9e0401"]]},{"id":"35acbef30630d644","type":"debug","z":"b92c72884452607b","name":"debug 77","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":480,"y":380,"wires":[]},{"id":"e159219189a042dc","type":"function","z":"b92c72884452607b","name":"集約(ble_pdh04)","func":"var cycle = flow.get('cycle');\nvar count = flow.get('count');\nvar count_max = flow.get('count_max');\nif (msg.payload.data[1] >= (2 ** 15 - 16) || count > 1024){\n    count = 0;\n    context.flow.array.fill(0);\n    node.warn(\"first data detexted(\"+String(msg.payload.data[1]+\")!!\"));\n} else {\n    context.flow.array[count]   = msg.payload.data[0];\n    context.flow.array[count+1] = msg.payload.data[1];\n    msg.payload.count = count;\n    msg.payload.cycle = cycle;\n    count = count+2;\n}\nflow.set('count', count);\nif (count == count_max){\n    msg.payload.data = context.flow.array;\n    cycle++;\n    flow.set('cycle', cycle);\n    return msg;\n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":340,"wires":[["fcddb3a469f1195c","d03001a5483a7983"]]},{"id":"5c5bb3b1c0f92cc0","type":"comment","z":"b92c72884452607b","name":"外部プログラム起動","info":"","x":130,"y":40,"wires":[]},{"id":"efe0b5481ecac62e","type":"comment","z":"b92c72884452607b","name":"画像表示","info":"","x":100,"y":440,"wires":[]},{"id":"496bc63a53e5165b","type":"mqtt-broker","name":"","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]

Appendix.5 FFT計算処理プログラム(python)

本文中で紹介しているPDHのBLEブログラムです。python 3.9.2で動作しています。
”fft_calc02.py”という名称です。Node-REDのexecノードでこの名称で実行していますので、ご注意ください。

import paho.mqtt.client as mqtt
import numpy as np
import matplotlib.pyplot as plt
import json

# MQTT ブローカーの設定
broker_address = "localhost"
port = 1883
topic = "fft_data"

# FFT Configulation
fs = 16000       # sampling frequency
N  = 1024       # number of samples
dt = 1/fs       # sampling period

# Window function
window = np.hanning(N)

def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    client.subscribe(topic)

def on_message(client, userdata, msg):
    dict_data = json.loads(msg.payload)
    y = np.array(dict_data["data"])
    y = y * window

    # FFTを実行
    y_fft = np.fft.fft(y)               # 離散フーリエ変換
    freq = np.fft.fftfreq(N, d=dt)      # 周波数を割り当てる
    Amp = abs(y_fft/(N/2))              # 音の大きさ(振幅の大きさ)

    # 窓補正
    acf=1/(sum(window)/N)
    Amp = acf*Amp

    output_FN = "/home/pi/Documents/pdm_imu/pictures/"+dict_data["datetime"]+".png"

# スレッドを開始
    ### 音波のスペクトル ###
    plt.plot(freq[1:int(N/2)], Amp[1:int(N/2)]) # A-f グラフのプロット
    plt.xlabel("frequency [Hz]")
    plt.ylabel("amplitude [V/rtHz]")
    plt.xscale("log")                           # 横軸を対数軸にセット
    plt.yscale("log")                           # 縦軸を対数軸にセット
    plt.savefig(output_FN, dpi=300)
    plt.clf()

    # NumPy配列をPythonリストに変換
    freq_list = freq[1:int(N/2)].tolist()
    amp_list = Amp[1:int(N/2)].tolist()

    # PythonリストをJSON形式に変換
    out_data = {"Num":N, "datatime":dict_data["datetime"], "freq": freq_list, "amp": amp_list}
    data_json = json.dumps(out_data)

    outFile = "/home/pi/Documents/pdm_imu/fftResult/"+dict_data["datetime"]+".json"
    # JSONデータをファイルに出力
    with open(outFile, "w") as f:
        f.write(data_json)

def main():
    # MQTT クライアントを作成
    mqtt_client = mqtt.Client()

    mqtt_client.on_connect = on_connect
    mqtt_client.on_message = on_message

    # MQTT ブローカーに接続
    mqtt_client.connect(broker_address, port)
    print(f"MQTT connected: {broker_address}")

    try:
        #while True:
        mqtt_client.loop_forever()
    except KeyboardInterrupt:
        # MQTT クライアントを切断
        mqtt_client.disconnect()
        print(f"MQTT disconnected: {broker_address}")

if __name__ == "__main__":
    main()