2020年7月24日、Taylor Roorda作成
はじめに
STのSensorTile.boxは、あらゆる経験レベルの開発者が利用できるように設計されたBluetoothセンサの完全なキットです。このボードには、温度、湿度、圧力などの環境用センサのほか、加速度計、ジャイロスコープ、磁力計、さらにはマイクなど、多数のセンサが搭載されています。このガイドでは、STの既製の機能パックを使用して、ファームウェアを記述することなくSensorTile.boxをクラウドに接続する1つの方法について手順を追って説明します。Raspberry Piは、Bluetooth接続からインターネット経由でデータを送信するゲートウェイとして機能します。ここでの目標は、SensorTile.boxのデータをクラウドデータベースに保存し、後で使用したり、インターネット対応のデバイスからアクセスできるようにすることです。この例では、AWSがクラウドプロバイダとして選択されており、DynamoDB、Lambda、およびIoT Greengrassサービスが利用されています。
ハードウェア要件
STEVAL-MKSBOX1V1 - ST SensorTile.box
ST-LINK/V2 - ST Link Programmer
RASPBERRY PI 3 MODEL B+ - Raspberry Pi 3 or 4
ソフトウェア要件と資料
BlueST SDK – BLE通信用Pythonライブラリ
FP-SNS-ALLMEMS1 – BLE接続、デジタルマイク、環境センサ、モーションセンサを搭載したIoTノード用SensorTile.boxファームウェア(v4.1.0またはそれ以前のバージョンでは互換性がありますが、以降のバージョンでは互換性はありません!)
STSW-LINK004 – プログラミング用ST Link Utility
Getting Started with AWS Greengrass – Raspberry PiでGreengrassを構成するためのAWSの資料
ファームウェアのセットアップ
このデモでは、STのFP-SNS-ALLMEMS1機能パックを使用して、SensorTile.boxセンサ(温度、湿度、圧力、磁力計、ジャイロスコープ、加速度センサ、マイク)からデータを読み取ります。また、アクティビティ認識(歩行、静止など)とジェスチャ認識の2つの追加機能も追跡します。STは、機能パックにスクリプトを含んでおり、以下に示すように、ボードにファームウェアを簡単に書き込むことができます。
- 機能パックを便利な場所に解凍し、ST Linkユーティリティがインストールされていることを確認します。
- ST-LINK/V2プログラマをSensorTile.boxボードに接続します。
- 解凍した機能パック内の以下のディレクトリに移動します。STM32CubeFunctionPack_ALLMEMS1_Vx.x.x\Projects\STM32L4R9ZI-SensorTile.box\Applications\ALLMEMS1\STM32CubeIDE
- CleanALLMEMS2_STM32CubeIDE_ST.box.batのコピーを作成し、プロジェクトのビルド結果の代わ りにビルド済みバイナリを使用するように変更します。コンパイルは不要です。アプリケーションバイナリNAMEALLMEMS1へのパスのみが変更されます。ダウンロードしたバージョンと一致していることを確認してください。
ModifiedCleanALLMEMS2_STM32CubeIDE_ST.box.bat
@echo off
set STLINK_PATH="C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\"
set NAMEALLMEMS1=..\Binary\STM32L4R9ZI-SensorTileBox_ALLMEMS1_v4.0.0
set BOOTLOADER="..\..\..\..\..\Utilities\BootLoader\STM32L4R9ZI\BootLoaderL4R9.bin"
color 0F
echo /******************************************/
echo Clean FP-SNS-ALLMEMS1
echo /******************************************/
echo Full Chip Erase
echo /******************************************/
%STLINK_PATH%ST-LINK_CLI.exe -c UR -Rst -ME
echo /******************************************/
echo Install BootLoader
echo /******************************************/
%STLINK_PATH%ST-LINK_CLI.exe -P %BOOTLOADER% 0x08000000 -V "after_programming"
echo /******************************************/
echo Install FP-SNS-ALLMEMS1
echo /******************************************/
%STLINK_PATH%ST-LINK_CLI.exe -P %NAMEALLMEMS1%.bin 0x08004000 -V "after_programming"
echo /******************************************/
echo Dump FP-SNS-ALLMEMS1 + BootLoader
echo /******************************************/
set offset_size=0x4000
for %%I in (%NAMEALLMEMS1%.bin) do set application_size=%%~zI
echo %NAMEALLMEMS1%.bin size is %application_size% bytes
set /a size=%offset_size%+%application_size%
echo Dumping %offset_size% + %application_size% = %size% bytes ...
echo ..........................
%STLINK_PATH%ST-LINK_CLI.exe -Dump 0x08000000 %size% %NAMEALLMEMS1%_BL.bin
echo /******************************************/
echo Reset STM32
echo /******************************************/
%STLINK_PATH%ST-LINK_CLI.exe -Rst
if NOT "%1" == "SILENT" pause
- ModifiedCleanALLMEMS2_STM32CubeIDE_ST.box.bat を実行し、ST Link Utilityを使用してブートローダとファームウェアイメージをボードにフラッシュ(保存)します。
プログラミング結果
/******************************************/
Clean FP-SNS-ALLMEMS1
/******************************************/
Full Chip Erase
/******************************************/
STM32 ST-LINK CLI v3.5.0.0
STM32 ST-LINK Command Line Interface
ST-LINK SN: 54FF6C064984485624491087
ST-LINK Firmware version: V2J27S6
Connected via SWD.
SWD Frequency = 4000K.
Target voltage = 1.8 V
Connection mode: Connect Under Reset
Reset mode: Hardware reset
Device ID: 0x470
Device flash Size: 2048 Kbytes
Device family: STM32L4Rx/L4Sx
MCU Reset.
Full chip erase...
Flash memory erased.
/******************************************/
Install BootLoader
/******************************************/
STM32 ST-LINK CLI v3.5.0.0
STM32 ST-LINK Command Line Interface
ST-LINK SN: 54FF6C064984485624491087
ST-LINK Firmware version: V2J27S6
Connected via SWD.
SWD Frequency = 4000K.
Target voltage = 1.8 V
Connection mode: Normal
Reset mode: Hardware reset
Device ID: 0x470
Device flash Size: 2048 Kbytes
Device family: STM32L4Rx/L4Sx
Loading file...
Flash Programming:
File : ..\..\..\..\..\Utilities\BootLoader\STM32L4R9ZI\BootLoaderL4R9.bin
Address : 0x08000000
Memory programming...
██████████████████████████████████████████████████ 100%
Reading and verifying device memory...
██████████████████████████████████████████████████ 100%
Memory programmed in 0s and 703ms.
Verification...OK
Programming Complete.
/******************************************/
Install FP-SNS-ALLMEMS1
/******************************************/
STM32 ST-LINK CLI v3.5.0.0
STM32 ST-LINK Command Line Interface
ST-LINK SN: 54FF6C064984485624491087
ST-LINK Firmware version: V2J27S6
Connected via SWD.
SWD Frequency = 4000K.
Target voltage = 1.8 V
Connection mode: Normal
Reset mode: Hardware reset
Device ID: 0x470
Device flash Size: 2048 Kbytes
Device family: STM32L4Rx/L4Sx
Loading file...
Flash Programming:
File : ..\Binary\STM32L4R9ZI-SensorTileBox_ALLMEMS1_v4.1.0.bin
Address : 0x08004000
Memory programming...
██████████████████████████████████████████████████ 100%
Reading and verifying device memory...
██████████████████████████████████████████████████ 100%
Memory programmed in 9s and 172ms.
Verification...OK
Programming Complete.
/******************************************/
Dump FP-SNS-ALLMEMS1 + BootLoader
/******************************************/
..\Binary\STM32L4R9ZI-SensorTileBox_ALLMEMS1_v4.1.0.bin size is 235972 bytes
Dumping 0x4000 + 235972 = 252356 bytes ...
..........................
STM32 ST-LINK CLI v3.5.0.0
STM32 ST-LINK Command Line Interface
ST-LINK SN: 54FF6C064984485624491087
ST-LINK Firmware version: V2J27S6
Connected via SWD.
SWD Frequency = 4000K.
Target voltage = 1.8 V
Connection mode: Normal
Reset mode: Hardware reset
Device ID: 0x470
Device flash Size: 2048 Kbytes
Device family: STM32L4Rx/L4Sx
Dumping memory ...
Address = 0x08000000
Memory Size = 0x0003D9C4
██████████████████████████████████████████████████ 100%
Saving file [..\Binary\STM32L4R9ZI-SensorTileBox_ALLMEMS1_v4.1.0_BL.bin] ...
Dumping memory to ..\Binary\STM32L4R9ZI-SensorTileBox_ALLMEMS1_v4.1.0_BL.bin succeded
/******************************************/
Reset STM32
/******************************************/
STM32 ST-LINK CLI v3.5.0.0
STM32 ST-LINK Command Line Interface
ST-LINK SN: 54FF6C064984485624491087
ST-LINK Firmware version: V2J27S6
Connected via SWD.
SWD Frequency = 4000K.
Target voltage = 1.8 V
Connection mode: Normal
Reset mode: Hardware reset
Device ID: 0x470
Device flash Size: 2048 Kbytes
Device family: STM32L4Rx/L4Sx
MCU Reset.
Press any key to continue . . .
Bluetoothの接続テスト
ゲートウェイデバイス(この場合はRaspberry Pi)で以下のステップを完了してください。
- GitHubの指示に従ってPython BlueST SDKをインストールします。
sudo pip3 install bluepy
sudo pip3 install futures
sudo pip3 install blue-st-sdk
- アプリケーション例のクローンを作成します。
git clone https://github.com/STMicroelectronics/BlueSTSDK_Python.git
- example_ble_1.pyを実行して、BLE経由でデータが取得できることを確認します。
BlueST SDKへのマイク機能の追加
この記事の執筆時点では、BLEデータにマイクのレベルを読み取るための特性が備わっているにもかかわらず、BlueST SDKにはマイクのレベルを読み取る機能クラスはありません。このセクションでは、SDKにカスタム機能を追加する簡単な方法を示します。
- 以下の内容で、feature_microphone.pyというPythonファイルを新規作成します。
feature_microphone.py
from blue_st_sdk.feature import Feature
from blue_st_sdk.feature import Sample
from blue_st_sdk.feature import ExtractedData
from blue_st_sdk.features.field import Field
from blue_st_sdk.features.field import FieldType
from blue_st_sdk.utils.number_conversion import LittleEndian
from blue_st_sdk.utils.blue_st_exceptions import BlueSTInvalidOperationException
from blue_st_sdk.utils.blue_st_exceptions import BlueSTInvalidDataException
import sys, traceback
class FeatureMicrophone(Feature):
"""The feature handles the data coming from a microphone.
Data is two bytes long and has one decimal value.
"""
FEATURE_NAME = "Microphone"
FEATURE_UNIT = "dB"
FEATURE_DATA_NAME = "Microphone"
DATA_MAX = 130 # Acoustic overload point of the microphone
DATA_MIN = 0
FEATURE_FIELDS = Field(
FEATURE_DATA_NAME,
FEATURE_UNIT,
FieldType.UInt8,
DATA_MAX,
DATA_MIN)
DATA_LENGTH_BYTES = 1
SCALE_FACTOR = 1.0
def __init__(self, node):
"""Constructor.
Args:
node (:class:`blue_st_sdk.node.Node`): Node that will send data to
this feature.
"""
super(FeatureMicrophone, self).__init__(self.FEATURE_NAME, node, [self.FEATURE_FIELDS])
def extract_data(self, timestamp, data, offset):
"""Extract the data from the feature's raw data.
Args:
timestamp (int): Data's timestamp.
data (str): The data read from the feature.
offset (int): Offset where to start reading data.
Returns:
:class:`blue_st_sdk.feature.ExtractedData`: Container of the number
of bytes read and the extracted data.
Raises:
:exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
if the data array has not enough data to read.
"""
if len(data) - offset < self.DATA_LENGTH_BYTES:
raise BlueSTInvalidDataException(
'There are no %d bytes available to read.' \
% (self.DATA_LENGTH_BYTES))
sample = Sample(
[data[offset] / self.SCALE_FACTOR],
self.get_fields_description(),
timestamp)
return ExtractedData(sample, self.DATA_LENGTH_BYTES)
@classmethod
def get_mic_level(self, sample):
"""Get the mic level value from a sample.
Args:
sample (:class:`blue_st_sdk.feature.Sample`): Sample data.
Returns:
float: The mic level value if the data array is valid, <nan>
otherwise.
"""
if sample is not None:
if sample._data:
if sample._data[0] is not None:
return float(sample._data[0])
return float('nan')
def read_mic_level(self):
"""Read the mic level value.
Returns:
float: The mic level value if the read operation is successful,
<nan> otherwise.
Raises:
:exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidOperationException`
is raised if the feature is not enabled or the operation
required is not supported.
:exc:`blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidDataException`
if the data array has not enough data to read.
"""
try:
self._read_data()
return FeatureMicrophone.get_mic_level(self._get_sample())
except (BlueSTInvalidOperationException, BlueSTInvalidDataException) as e:
raise e
- カスタム機能を使用するためにSTのサンプルを修正します。
mic_test.py
import os
import sys
import time
from blue_st_sdk.manager import Manager, ManagerListener
from blue_st_sdk.node import NodeListener
from blue_st_sdk.feature import FeatureListener
from blue_st_sdk.features.audio.adpcm.feature_audio_adpcm import FeatureAudioADPCM
from blue_st_sdk.features.audio.adpcm.feature_audio_adpcm_sync import FeatureAudioADPCMSync
from blue_st_sdk.utils.uuid_to_feature_map import UUIDToFeatureMap
from blue_st_sdk.utils.ble_node_definitions import FeatureCharacteristic
from feature_microphone import FeatureMicrophone
SCANNING_TIME_s = 5
MAX_NOTIFICATIONS = 15
DEVICE_NAME = "AM1V400"
class MyManagerListener(ManagerListener):
#
# This method is called whenever a discovery process starts or stops.
#
# @param manager Manager instance that starts/stops the process.
# @param enabled True if a new discovery starts, False otherwise.
#
def on_discovery_change(self, manager, enabled):
print('Discovery %s.' % ('started' if enabled else 'stopped'))
if not enabled:
print()
#
# This method is called whenever a new node is discovered.
#
# @param manager Manager instance that discovers the node.
# @param node New node discovered.
#
def on_node_discovered(self, manager, node):
print('New device discovered: %s.' % (node.get_name()))
class MyNodeListener(NodeListener):
#
# To be called whenever a node connects to a host.
#
# @param node Node that has connected to a host.
#
def on_connect(self, node):
print('Device %s connected.' % (node.get_name()))
#
# To be called whenever a node disconnects from a host.
#
# @param node Node that has disconnected from a host.
# @param unexpected True if the disconnection is unexpected, False otherwise
# (called by the user).
#
def on_disconnect(self, node, unexpected=False):
print('Device %s disconnected%s.' % \
(node.get_name(), ' unexpectedly' if unexpected else ''))
if unexpected:
# Exiting.
print('\nExiting...\n')
sys.exit(0)
class MyFeatureListener(FeatureListener):
_notifications = 0
"""Counting notifications to print only the desired ones."""
#
# To be called whenever the feature updates its data.
#
# @param feature Feature that has updated.
# @param sample Data extracted from the feature.
#
def on_update(self, feature, sample):
if self._notifications < MAX_NOTIFICATIONS:
self._notifications += 1
print(feature)
def main():
try:
# Creating Bluetooth Manager.
manager = Manager.instance()
manager_listener = MyManagerListener()
manager.add_listener(manager_listener)
# Append custom mic level feature into the BlueST library before discovery
mask_to_features_dic = FeatureCharacteristic.SENSOR_TILE_BOX_MASK_TO_FEATURE_DIC
mask_to_features_dic[0x04000000] = FeatureMicrophone
try:
Manager.add_features_to_node(0x06, mask_to_features_dic)
except Exception as e:
print(e)
# Synchronous discovery of Bluetooth devices.
print('Scanning Bluetooth devices...\n')
manager.discover(SCANNING_TIME_s)
# Getting discovered devices.
discovered_devices = manager.get_nodes()
if not discovered_devices:
raise Exception("No devices found.")
# Find the correct device name
filtered_devices = list(filter(lambda x: x.get_name() == DEVICE_NAME, discovered_devices))
# Only have the one device right now
device = filtered_devices[0]
print("Connecting to device: {} ({})\n".format(device.get_name(), device.get_tag()))
node_listener = MyNodeListener()
device.add_listener(node_listener)
if not device.connect():
print('Connection failed.\n')
raise Exception("Failed to connect to device {} ({}).".format(device.get_name(), device.get_tag()))
# Getting features.
features = device.get_features()
print("Available features:")
for each in features:
print(each.get_name())
# Get only the mic feature
filtered_features = list(filter(lambda x: x.get_name() == "Microphone", features))
feature = filtered_features[0]
print("Listening to {} feature...".format(feature.get_name()))
# Enabling notifications.
feature_listener = MyFeatureListener()
feature.add_listener(feature_listener)
device.enable_notifications(feature)
# Handling audio case (both audio features have to be enabled).
if isinstance(feature, FeatureAudioADPCM):
audio_sync_feature_listener = MyFeatureListener()
audio_sync_feature.add_listener(audio_sync_feature_listener)
device.enable_notifications(audio_sync_feature)
elif isinstance(feature, FeatureAudioADPCMSync):
audio_feature_listener = MyFeatureListener()
audio_feature.add_listener(audio_feature_listener)
device.enable_notifications(audio_feature)
# Getting notifications.
notifications = 0
start_time = time.time()
while notifications < MAX_NOTIFICATIONS:
if device.wait_for_notifications(0.05):
start_time = time.time()
notifications += 1
if time.time() > start_time + 10:
print("Timed out waiting for notifications.")
break
print("Shutting down...")
# Disabling notifications.
device.disable_notifications(feature)
feature.remove_listener(feature_listener)
# Handling audio case (both audio features have to be disabled).
if isinstance(feature, FeatureAudioADPCM):
device.disable_notifications(audio_sync_feature)
audio_sync_feature.remove_listener(audio_sync_feature_listener)
elif isinstance(feature, FeatureAudioADPCMSync):
device.disable_notifications(audio_feature)
audio_feature.remove_listener(audio_feature_listener)
# Shut everything down
device.remove_listener(node_listener)
device.disconnect()
manager.remove_listener(manager_listener)
except KeyboardInterrupt:
print("Program killed. Exiting...")
device.disable_notifications(feature)
feature.remove_listener(feature_listener)
device.remove_listener(node_listener)
device.disconnect()
manager.remove_listener(manager_listener)
sys.exit(0)
except SystemExit:
os._exit(0)
if __name__ == "__main__":
main()
注意:次のステップでmic_test.pyを実行する前に対処しなければならないBlueST SDKのバグがあるようです。手順については 以下の投稿を参照してください。
- それが動作することを確認してください。
この時点で、Pythonスクリプトを通じてすべての基本センサデータにアクセスできるようになります。 次のステップは、データをAWSに記録し、最終的にそれを使って何か有益なことを見つけることです。
Raspberry PiでのAWS Greengrass Coreのセットアップ
この例では、Raspberry PiがAWS IoT Greengrass Coreをホストします。これにより、Raspberry Piがクラウドへのゲートウェイとして機能し、AWSとの通信が簡素化されます。また、Lambdaコード、メッセージング、その他の機能をローカルレベルで実行できるため、IoTデバイスはクラウド接続なしでも動作および対話を続けることができます。AWSには、初期セットアップのための優れたガイドが既に用意されています。以下に続きます。
- スタートガイドにある以下のいずれかの方法でRaspberry Piをセットアップします。
a. 自動クイックスタートスクリプト 、もしくは
b. モジュール1 (Greengrass環境環境のセットアップ)とモジュール2(Greengrass Coreソフトウェアのインストール)
c. 電源投入時にGreengrass Daemonが起動するように構成します。
先に進む前に、以下の結果が得られていることを確認してください。
- AWS IoTの新しい Greengrass Group
- Raspberry Piのセキュリティリソース(証明書、鍵、設定)
- Raspberry Pi上でGreengrass Daemonが動作している
SensorTile.boxのAWSデバイスの作成
次に、SensorTile.boxを一意に識別するためにデバイスを作成します。
- 最後のステップのGreengrass Groupで、新しいデバイスを作成します。
- セキュリティリソースをダウンロードし、Raspberry Piの便利な場所にコピーします。この例では、認証情報を保持するためのawsというディレクトリがプロジェクトディレクトリにあると仮定します。
- 上記の例に基づいて、マイク機能を追加した SensorTile.boxクラスを作成します。
sensortile_box.py
import os
import sys
import time
from blue_st_sdk.manager import Manager, ManagerListener
from blue_st_sdk.node import NodeListener
from blue_st_sdk.feature import FeatureListener
from blue_st_sdk.features.audio.adpcm.feature_audio_adpcm import FeatureAudioADPCM
from blue_st_sdk.features.audio.adpcm.feature_audio_adpcm_sync import FeatureAudioADPCMSync
from blue_st_sdk.utils.uuid_to_feature_map import UUIDToFeatureMap
from blue_st_sdk.utils.ble_node_definitions import FeatureCharacteristic
from feature_microphone import FeatureMicrophone
class BluetoothError(Exception):
def __init__(self, value):
self.value = value
# Default listner to print notifications from BT
class Listener(FeatureListener):
def on_update(self, feature, sample):
print(feature)
class SensorTileBox:
DEVICE_NAME = "AM1V400"
SCAN_TIME_s = 3
TIMEOUT_s = 10
# Maps feature name to its characteristic handle
FEATURES = {
"Gesture": 0,
"Activity Recognition": 1,
"Temperature1": 2,
"Temperature2": 3,
"Humidity": 4,
"Pressure": 5,
"Magnetometer": 6,
"Gyroscope": 7,
"Accelerometer": 8,
"Microphone": 9
}
def __init__(self, bt_addr):
# Creating Bluetooth Manager.
self.bt_manager = Manager.instance()
# Append custom mic level feature into the BlueST library before discovery
mask_to_features_dic = FeatureCharacteristic.SENSOR_TILE_BOX_MASK_TO_FEATURE_DIC
mask_to_features_dic[0x04000000] = FeatureMicrophone
try:
Manager.add_features_to_node(0x06, mask_to_features_dic)
except Exception as e:
print(e)
# Synchronous discovery of Bluetooth devices.
self.bt_manager.discover(SensorTileBox.SCAN_TIME_s)
discovered_devices = self.bt_manager.get_nodes()
if not discovered_devices:
raise BluetoothError("No devices discovered.")
# Find the correct device name
filtered_devices = list(filter(lambda x: x.get_name() == SensorTileBox.DEVICE_NAME, discovered_devices))
if not filtered_devices:
raise BluetoothError("Could not find device named {}".format(SensorTileBox.DEVICE_NAME))
# Use the first (presumably only) matched device
self.bt_device = filtered_devices[0]
if not self.bt_device.connect():
raise BluetoothError("Could not connect to device {} ({})".format(self.bt_device.get_name(), self.bt_device.get_tag()))
# Retrieve available features from the device, indexed by characteristic handle
self.features = self.bt_device.get_features()
def attach_feature_listener(self, feature_name, listener):
# Register a callback function to process incoming updates from the specified feature
feature = self.features[SensorTileBox.FEATURES[feature_name]]
feature.add_listener(listener)
def remove_feature_listener(self, feature_name, listener):
# Register a callback function to process incoming updates from the specified feature
feature = self.features[SensorTileBox.FEATURES[feature_name]]
feature.remove_listener(listener)
def enable_feature_notifications(self, feature_name):
feature = self.features[SensorTileBox.FEATURES[feature_name]]
self.bt_device.enable_notifications(feature)
def disable_feature_notifications(self, feature_name):
feature = self.features[SensorTileBox.FEATURES[feature_name]]
self.bt_device.disable_notifications(feature)
def start(self):
while True:
if self.bt_device.wait_for_notifications(SensorTileBox.TIMEOUT_s):
# Notification Received
continue
else:
raise BluetoothError("ERROR: Notifications timed out after {} seconds.".format(SensorTileBox.TIMEOUT_s))
def shutdown(self):
# Disconnect and reset, removing all notifications and listeners
for f in self.features:
self.bt_device.disable_notifications(f)
for l in f._listeners:
f.remove_listener(l)
print("Disconnecting from SensorTile.box.")
self.bt_manager.reset_discovery()
self.bt_device.disconnect()
- Greengrassの「デバイス」として機能するスクリプトを作成します。セットアップに一致するように、必要に応じてBluetooth MACアドレス、AWSホスト URL、証明書/キーのパスを必ず調整してください。
gg_sensortile_box.py
# AWS IoT Device
# Creates an instance of the SensorTileBox and registers a listener
# that translates incoming BT notifications to JSON MQTT messages
# which are then sent to the Greengrass Core
import json
import logging
import os
import re
import sys
import time
from AWSIoTPythonSDK.core.greengrass.discovery.providers import DiscoveryInfoProvider
from AWSIoTPythonSDK.core.protocol.connection.cores import ProgressiveBackOffCore
from AWSIoTPythonSDK.exception.AWSIoTExceptions import DiscoveryInvalidRequestException
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient, AWSIoTMQTTShadowClient
from blue_st_sdk.feature import FeatureListener
from sensortile_box import SensorTileBox, BluetoothError
# Custom listener to convert BT notifcations to MQTT
class NotificationListener(FeatureListener):
def __init__(self, mqtt_client, downsample=1):
super().__init__()
self.mqtt_client = mqtt_client
# Downsample by this factor for rate limiting
self.downsample = downsample
self._counter = 0
def on_update(self, feature, sample):
# Convert data to json format and send over MQTT
json_dict = {}
json_dict["feature"] = feature.get_name()
json_dict["timestamp"] = sample.get_timestamp()
json_dict["data"] = sample.get_data()
# Downsample if enabled
self._counter += 1
if self._counter >= self.downsample:
json_string = json.dumps(json_dict)
myMQTTClient.publish("/device/test", json_string, 0)
self._counter = 0
# Create a SensorTileBox instance to handle the bluetooth connection
# Do this first because if the connection fails, nothing else matters
print("Establishing Bluetooth connection to SensorTile.box")
BLUETOOTH_ADDR = "E7:BE:F2:3D:1C:DA".lower()
stb = SensorTileBox(BLUETOOTH_ADDR)
# Connect with AWS IoT Greengrass Core (the RPi)
# TODO: these paths must be adjusted for your own credentials/system
MAX_DISCOVERY_RETRIES = 10 # MAX tries at discovery before giving up
GROUP_PATH = "./aws/GroupCA" # directory storing discovery info
CA_NAME = "root-ca.crt" # stores GGC CA cert
GGC_ADDR_NAME = "ggc-host" # stores GGC host address
host = "<your aws host endpoint>"
iotCAPath = "/greengrass/certs/root.ca.pem"
certificatePath = "./aws/<yourcredentials>.cert.pem"
privateKeyPath = "./aws/<yourcredentials>.private.key"
thingName = "SensorTile-box"
clientId = "SensorTile-box"
# AWS Example for Greegrass Discovery
# Configure logging
logger = logging.getLogger("AWSIoTPythonSDK.core")
logger.setLevel(logging.INFO) # set to logging.DEBUG for additional logging
streamHandler = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
streamHandler.setFormatter(formatter)
logger.addHandler(streamHandler)
# function does basic regex check to see if value might be an ip address
def isIpAddress(value):
match = re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}", value)
return True if match else False
# function reads host GGC ip address from filePath
def getGGCAddr(filePath):
f = open(filePath, "r")
return f.readline()
# Used to discover GGC group CA and end point. After discovering it persists in GROUP_PATH
def discoverGGC(host, iotCAPath, certificatePath, privateKeyPath, clientId):
# Progressive back off core
backOffCore = ProgressiveBackOffCore()
# Discover GGCs
discoveryInfoProvider = DiscoveryInfoProvider()
discoveryInfoProvider.configureEndpoint(host)
discoveryInfoProvider.configureCredentials(iotCAPath, certificatePath, privateKeyPath)
discoveryInfoProvider.configureTimeout(10) # 10 sec
print("Iot end point: " + host)
print("Iot CA Path: " + iotCAPath)
print("GGAD cert path: " + certificatePath)
print("GGAD private key path: " + privateKeyPath)
print("GGAD thing name : " + clientId)
retryCount = MAX_DISCOVERY_RETRIES
discovered = False
groupCA = None
coreInfo = None
while retryCount != 0:
try:
discoveryInfo = discoveryInfoProvider.discover(clientId)
caList = discoveryInfo.getAllCas()
coreList = discoveryInfo.getAllCores()
# In this example we only have one core
# So we pick the first ca and core info
groupId, ca = caList[0]
coreInfo = coreList[0]
print("Discovered GGC: " + coreInfo.coreThingArn + " from Group: " + groupId)
hostAddr = ""
# In this example Ip detector lambda is turned on which reports
# the GGC hostAddr to the CIS (Connectivity Information Service) that stores the
# connectivity information for the AWS Greengrass core associated with your group.
# This is the information used by discovery and the list of host addresses
# could be outdated or wrong and you would normally want to
# validate it in a better way.
# For simplicity, we will assume the first host address that looks like an ip
# is the right one to connect to GGC.
# Note: this can also be set manually via the update-connectivity-info CLI
for addr in coreInfo.connectivityInfoList:
hostAddr = addr.host
if isIpAddress(hostAddr):
break
print("Discovered GGC Host Address: " + hostAddr)
print("Now we persist the connectivity/identity information...")
groupCA = GROUP_PATH + CA_NAME
ggcHostPath = GROUP_PATH + GGC_ADDR_NAME
if not os.path.exists(GROUP_PATH):
os.makedirs(GROUP_PATH)
groupCAFile = open(groupCA, "w")
groupCAFile.write(ca)
groupCAFile.close()
groupHostFile = open(ggcHostPath, "w")
groupHostFile.write(hostAddr)
groupHostFile.close()
discovered = True
print("Now proceed to the connecting flow...")
break
except DiscoveryInvalidRequestException as e:
print("Invalid discovery request detected!")
print("Type: " + str(type(e)))
print("Error message: " + e.message)
print("Stopping...")
break
except BaseException as e:
print("Error in discovery!")
print("Type: " + str(type(e)))
print("Error message: " + e.message)
retryCount -= 1
print("\n" + str(retryCount) + "/" + str(MAX_DISCOVERY_RETRIES) + " retries left\n")
print("Backing off...\n")
backOffCore.backOff()
if not discovered:
print("Discovery failed after " + str(MAX_DISCOVERY_RETRIES) + " retries. Exiting...\n")
sys.exit(-1)
# Run Discovery service to check which GGC to connect to, if it hasn't been run already
# Discovery talks with the IoT cloud to get the GGC CA cert and ip address
if not os.path.isfile("./auth/groupCA/root-ca.crt"):
discoverGGC(host, iotCAPath, certificatePath, privateKeyPath, clientId)
else:
print("Greengrass core has already been discovered.")
# read GGC Host Address from file
ggcAddrPath = GROUP_PATH + GGC_ADDR_NAME
rootCAPath = GROUP_PATH + CA_NAME
ggcAddr = getGGCAddr(ggcAddrPath)
print("GGC Host Address: " + ggcAddr)
print("GGC Group CA Path: " + rootCAPath)
print("Private Key of SensorTileBox thing Path: " + privateKeyPath)
print("Certificate of SensorTileBox thing Path: " + certificatePath)
print("Client ID(thing name for SensorTileBox): " + clientId)
print("Target shadow thing ID(thing name for SensorTileBox): " + thingName)
# Discovery complete. End of AWS Examples
# Create an MQTT client
myMQTTClient = AWSIoTMQTTClient(clientId)
myMQTTClient.configureEndpoint(ggcAddr, 8883)
myMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath)
# Configure MQTT parameters (example defaults)
myMQTTClient.configureOfflinePublishQueueing(-1) # Infinite offline Publish queueing
myMQTTClient.configureDrainingFrequency(2) # Draining: 2 Hz
myMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
myMQTTClient.configureConnectDisconnectTimeout(10) # 10 sec
myMQTTClient.configureMQTTOperationTimeout(5) # 5 sec
myMQTTClient.connect()
# Enable notifications for desired sensors to be sent to an MQTT listener
temp1_listener = NotificationListener(myMQTTClient, downsample=6)
stb.attach_feature_listener("Temperature1", temp1_listener)
stb.enable_feature_notifications("Temperature1")
# temp2_listener = NotificationListener(myMQTTClient)
# stb.attach_feature_listener("Temperature2", temp2_listener)
# stb.enable_feature_notifications("Temperature2")
# humidity_listener = NotificationListener(myMQTTClient)
# stb.attach_feature_listener("Humidity", humidity_listener)
# stb.enable_feature_notifications("Humidity")
#
# pressure_listener = NotificationListener(myMQTTClient)
# stb.attach_feature_listener("Pressure", pressure_listener)
# stb.enable_feature_notifications("Pressure")
#
# magnet_listener = NotificationListener(myMQTTClient)
# stb.attach_feature_listener("Magnetometer", magnet_listener)
# stb.enable_feature_notifications("Magnetometer")
#
# gyro_listener = NotificationListener(myMQTTClient)
# stb.attach_feature_listener("Gyroscope", gyro_listener)
# stb.enable_feature_notifications("Gyroscope")
#
# accel_listener = NotificationListener(myMQTTClient)
# stb.attach_feature_listener("Accelerometer", accel_listener)
# stb.enable_feature_notifications("Accelerometer")
mic_listener = NotificationListener(myMQTTClient, downsample=6)
stb.attach_feature_listener("Microphone", mic_listener)
stb.enable_feature_notifications("Microphone")
# Test message
myMQTTClient.publish("/device/hello", "Hello from SensorTile!", 0)
# Stay here until killed then shutdown
try:
stb.start()
except BluetoothError as e:
print(e)
print("Stopping SensorTile...")
except KeyboardInterrupt:
print("Keyboard interrupt received. Shutting down...")
# Close and reset everything
stb.shutdown()
myMQTTClient.disconnect()
sys.exit(0)
AWSサービスの設定
AWS LambdaとDynamoDBはこのプロジェクトで使われる主要なサービスです。Lambda関数はRaspberry Pi上でローカルに実行され、入力されたデータをDynamoDBに書き込む前にフォーマットします。
Lambda関数
このプロジェクトのLambda関数は、AWS Python SDK (boto3)を使用して、DynamoDBに簡単に接続します。通常、boto3はLambda実行環境に含まれていますが、Greengrassではそうではありません。 したがって、Lambda関数が持つ可能性のある追加の依存関係を含むデプロイパッケージを作成する必要があります。このデメリットは、Lambdaコードを変更すると、デプロイパッケージ全体を再アップロードする必要があることです。
- 依存関係とLambdaコードを格納する新しいディレクトリを作成します。
- 新しいディレクトリにboto3の依存関係をPip installします。
pip3 install boto3 -t <target directory>
- Lambda関数のソースコードを追加します。必要に応じて領域を調整します。
lambda_function.py
# Lambda function that runs on the Greengrass Core
import json
import logging
from datetime import datetime
import boto3
from botocore.exceptions import ClientError
# Connect to DynamoDB
dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
table_name = "SensorTileBoxData"
# Initialize logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Create the dynamoDB table if needed
try:
table = dynamodb.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "timestamp", "KeyType": "HASH"}, {"AttributeName": "feature", "KeyType": "RANGE"}], # Partition and sort keys
AttributeDefinitions=[{"AttributeName": "timestamp", "AttributeType": "S"}, {"AttributeName": "feature", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
# Wait until the table exists.
table.meta.client.get_waiter("table_exists").wait(TableName=table_name)
except ClientError as e:
if e.response["Error"]["Code"] == "ResourceInUseException":
print("Table already created")
# Use the existing table
table = dynamodb.Table(table_name)
else:
raise e
def lambda_handler(event, context):
global table
logger.info(event)
# Convert data to strings because Dynamo doesn't seem to like numeric types
date_string = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
table.put_item(
Item={
"timestamp": date_string,
"stb_time": str(event["timestamp"]),
"feature": event["feature"],
"data": str(event["data"])
}
)
- 適当な名前のPython 3.7 Lambda関数を新規作成し、zip圧縮したディレクトリをアップロードします。
- 上部のActionsドロップダウンメニューから、Publish new versionを選択します。必要に応じて、説明を追加してください。Greengrassでは、公開されたバージョンのLambda関数のみを使用することができます。
- (オプション)トップのActionsドロップダウンメニューから、Create an aliasを選択します。エイリアスはGreengrassのLambda関数の設定に表示され、場合によってはバージョン番号よりも参照しやすいです。これはAmazonによって推奨されていますが、このチュートリアルでは使用しません。
このLambda関数は、Greengrass Coreデバイスで利用できるようになりました。
DynamoDB
次に、データのタイムスタンプをパーティション(ハッシュ)キー、機能名をソートキーとしてDynamoDBのテーブルが作成されます。これにより、データベース内に同じタイムスタンプを持つ複数の機能が存在できるようになります。ソートキーがない場合は、特定のタイムスタンプを持つ最新のデータポイントのみが保存されます。テーブルを作成すれば、Greengrassグループにテーブルへのアクセスを許可するパーミッションを与える必要があります。
-
以下のように新しいテーブルを作成します。これを今すぐ行わない場合、Lambda関数の初回実行時にテーブルが作成されますが、この後のパーミッションポリシーで特定のテーブルを指定することはできません。
-
AWS IAMコンソールに移動し、サイドバーからPoliciesを選択します。
-
Create policyを選択します。「SensorTileBoxData」というテーブルに対してのみGreengrassにCreateTable、PutItem、DescribeTable権限を提供する以下のJSON(JavaScript Object Notation)を入力します。
Policy JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Tutorial Permissions",
"Effect": "Allow",
"Action": [
"dynamodb:CreateTable",
"dynamodb:PutItem",
"dynamodb:DescribeTable"
],
"Resource": "arn:aws:dynamodb:*:*:table/SensorTileBoxData"
}
]
}
- Review policyを選択します。Review policyに greengrass_SensorBox_Table などの名前を付けて、Review policyの作成を終了します。
- IAM コンソールに戻り、サイドバーからRoleを選択します。
- Create roleを選択します。
- Amazon ServiceとしてGreengrassを選択します。Next: Permissionsを押します。
- Create roleにアタッチする新しいReview policyを探します。
- Next:Permissionsを2回選択してレビュー画面に進みます。Create roleにGreengrass_SensorTile_Group_Roleのような名前を付け、Create roleの作成を完了します。
Greengrassグループの設定と展開
最後のステップは、上記で作成したすべての構成をGreengrassグループに割り当て、それをRaspberry Piに展開することです。
- AWS IoT Consoleで、Greengrassセクションに移動し、先ほど作成したグループを選択します。
- Greengrassグループのページで、サイドバーからSettingsを選択し、Group RoleでAdd Roleを選択します。上記で作成したRoleを選択します。結果は以下のようになるはずです。
- グループのサイドバーからLambdasを選択し、Add Lambdaを選択します。.
- 上記のロギングLambda関数を追加します。選択できるバージョンやエイリアスは1つだけです。
- Lambdas ページに戻って、新しく追加した関数のellipsis (…) ボタンを選択します。Edit Configurationを選択します。
- Lambda lifecycleで、「Make this function long-lived and keep it running indefinitely」を選択します。そして、Updateを選択して変更を保存します。
- グループのサイドバーから、Subscriptions を選択し、Add Subscriptionを選択します。
- Sourceに、Devices → SensorTile-boxを選択します。Targetには、Lambda → GG_CloudLog(または関数の名前)を選択します。Nextを押します。
- トピックフィルタとして /device/testを入力します。Next(次へ)を押し、Finish(完了)を押します。
- 上記の手順を繰り返して別のサブスクリプションを追加しますが、Target to Services → IoT Cloudに変更します。これはデモには必要ありませんが、デバッグに便利なAWSコンソールでMQTTメッセージを表示できるようにします。
- グループのDeploymentsタブに戻ります。 ActionsメニューからDeployを選択します。
すべてが順調に進むと、「正常に完了しました」という緑色の点が表示されます。
測定する
クラウドへのパスが構成されたので、最後のステップはそれを利用することです。ここでは、上記で作成したデバイススクリプトでデータを計測し、AWS経由でそのデータにアクセスする簡単な例を紹介します。
-
計測する機能を選択します。すべての基本的なセンサは、上記のgg_sensortile_box.pyスクリプトで利用可能です。必要に応じてコメントイン、コメントアウトすることができます。デフォルトでは、このスクリプトはアパート内のギターアンプの温度とサウンドレベルを測定します。どちらの機能も、DynamoDBのデフォルトの書き込み容量である5件/秒に収まるように、6倍にダウンサンプリングされています。
-
gg_sensortile_box.pyを好きなだけ実行します。Ctrl+Cでスクリプトを終了します。スクリプトは、10秒間にSensorTIle.boxからBLE通知が検出されなかった場合にも停止します。
DeviceScriptの実行
sudo python3 gg_sensortile_box.py
Establishing Bluetooth connection to SensorTile.box
Greengrass core has already been discovered.
GGC Host Address: 127.0.0.1
GGC Group CA Path: ./aws/groupCA/root-ca.crt
Private Key of SensorTileBox thing Path: ./aws/<yourkey>.private.key
Certificate of SensorTileBox thing Path: ./aws/<yourkey>.cert.pem
Client ID(thing name for SensorTileBox): SensorTile-box
Target shadow thing ID(thing name for SensorTileBox): SensorTile-box
2020-07-23 15:58:58,350 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - MqttCore initialized
2020-07-23 15:58:58,351 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Client id: SensorTile-box
2020-07-23 15:58:58,351 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Protocol version: MQTTv3.1.1
2020-07-23 15:58:58,352 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Authentication type: TLSv1.2 certificate based Mutual Auth.
2020-07-23 15:58:58,353 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Configuring endpoint...
2020-07-23 15:58:58,353 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Configuring certificates...
2020-07-23 15:58:58,355 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Configuring offline requests queueing: max queue size: -1
2020-07-23 15:58:58,356 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Configuring offline requests queue draining interval: 0.500000 sec
2020-07-23 15:58:58,357 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Configuring reconnect back off timing...
2020-07-23 15:58:58,358 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Base quiet time: 1.000000 sec
2020-07-23 15:58:58,359 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Max quiet time: 32.000000 sec
2020-07-23 15:58:58,360 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Stable connection time: 20.000000 sec
2020-07-23 15:58:58,360 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Configuring connect/disconnect time out: 10.000000 sec
2020-07-23 15:58:58,361 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Configuring MQTT operation time out: 5.000000 sec
2020-07-23 15:58:58,362 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync connect...
2020-07-23 15:58:58,362 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing async connect...
2020-07-23 15:58:58,363 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Keep-alive: 600.000000 sec
2020-07-23 15:58:58,685 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:58:58,929 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:58:59,222 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:58:59,514 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:58:59,807 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:00,002 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:00,100 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:00,391 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:00,733 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:01,025 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:01,318 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:01,610 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:01,757 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:01,903 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:02,195 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:02,488 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:02,878 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:03,122 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
^CKeyboard interrupt received. Shutting down...
2020-07-23 15:59:03,414 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:03,707 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
2020-07-23 15:59:03,999 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync publish...
Disconnecting from SensorTile.box.
2020-07-23 15:59:04,148 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing sync disconnect...
2020-07-23 15:59:04,148 - AWSIoTPythonSDK.core.protocol.mqtt_core - INFO - Performing async disconnect...
- DynamoDBのコンソールを使用して、データベースにエントリが追加されていることを確認します。
- プロットスクリプトの依存関係をインストールします。
pip3 install boto3 matplotlib pandas
- 以下のPythonスクリプトを使用してデータを読み込み、プロットします。
stbgraphy.py
# Parse and plot data from DynamoDB
import boto3
from boto3.dynamodb.conditions import Attr
import json
import pandas as pd
import matplotlib.pyplot as plt
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('SensorTileBoxData')
# Get the whole table (if it's not very big)
# response = table.scan()
# Or filter on specific attributes
response = table.scan(
FilterExpression=Attr('timestamp').contains('2020-07-23 17')
)
items = response['Items']
print("Scanned {} items.".format(len(items)))
df = pd.read_json(json.dumps(items))
sorted_frame = df.sort_values(by=['timestamp'], ignore_index=True)
# Remove brackets and cast to numeric type
sorted_frame['data'] = pd.to_numeric(sorted_frame['data'].apply(lambda s: s.replace('[', '').replace(']', '')))
# Separate frame by feature and plot
mic_data = sorted_frame[sorted_frame.values == 'Microphone']
mic_data.plot(x='timestamp', y='data', title='Microphone Level (dB)')
temp_data = sorted_frame[sorted_frame.values == 'Temperature']
temp_data.plot(x='timestamp', y='data', title='Temperature (C)')
plt.show()
- 出来上がったプロットを見てみましょう。
上のプロットのデータは、アンプがスタンバイ状態で起動している状態で、SensorTile.boxをアンプの通気口の近くに配置することによって生成されました。 最初のデータは、40dBの典型的な室内騒音で、再生中にはほぼ一定の87dBまで上昇します。 下向きのスパイクは、再生中の一時的な停止を示します。 最後の1分間では、SensorTile.boxをアンプのある部屋の外に移動し、再生を続けながらドアを閉めると、部屋の外のレベルが妥当な55dBに下がります。 温度測定では、真空管が温まるにつれて室温から着実に上昇し、部屋を離れると下がっていくことが示されています。
まとめ
このチュートリアルでは、AWS Greengrassを使用してSensorTile.boxをクラウドに接続する方法を説明しました。SensorTile.boxのプラグアンドプレイの性質を利用し、最小限のオーバーヘッドとファームウェアを開発することなく、様々なセンサにアクセスします。 クラウドサービスを追加するには、ローカルアプローチよりも複雑なセットアップが必要ですが、その代わりに柔軟性と拡張性が高まります。複数の SensorTile.boxなどの追加デバイスを簡単に追加または削除して、より多くの測定を行うことができます。 デバイスは、MQTTサブスクリプションを通じて相互に安全に通信します。 追加のデータ処理や他のサービスに接続するには、追加の Lambda関数 を使用します。
milan.vercammen
3月 23 日
こんにちは、このフォーラムをフォローしようとしていますが、STEVAL-MKSBOX1V1にファームウェアを書き込むことには成功したものの、その後SensorTile.boxをRaspberry Piに接続しようとすると、Bluetoothがデバイスを見つけることができません。最初はファームウェアの設定中に何かがうまくいかなかったのかと思いましたが、私の携帯電話を使って接続してみると、新しいファームウェアが適用されたデバイスをすぐに見つけることができます。
どなたか、この問題の原因がわかる方はいらっしゃいますか?
Verna_1353 DigiKey Employee
3月23日
テクニカルフォーラムへようこそ。この件については問題がわからないので、私は対応できません。Mattがこの投稿を見て対応するはずです。
Matt_Mielke DigiKey Employee
3月23日
こんにちは @milan.vercammen さん
どのバージョンのRaspberry Piを使用していますか? example_ble_1.py
を実行したときに観測される出力を提供できますか?どの携帯電話(スマートフォン)を使用していて、どのようにしてSensorTileボードに接続していますか? この方法で利用可能な機能を見ることができますか?
ありがとうございます。
Matt
milan.vercammen
3月25日
こんにちは @Matt_Mielke さん
Raspberry Pi Model 3 Bを使用しています(SensorTile.boxのマイクデータを使用して保存するために接続したいデバイスですが、なぜかできません)。そして、接続できるかどうかを確認するために使用した携帯電話はSamsung Galaxy A22 5Gで、このためにアプリST BLE Sensor Classicを使用しました。ファームウェアを書き込んだ後、Raspberry Piと接続しようとしたときに、次の出力が得られます:
guest@raspberrypi:~ $ sudo python3 example_ble_1.py
##################
BlueST Example
##################
Scanning Bluetooth devices…
Discovery started.
Discovery stopped.
No Bluetooth devices found. Exiting…
If I’m trying to connect the device without the firmware, then I get one of the two errors,it immediately disconnects when I select the device, or number two, it gives me an error before I can select a device.
Error output one:
guest@raspberrypi:~ $ sudo python3 example_ble_1.py
##################
BlueST Example
##################
Scanning Bluetooth devices…
Discovery started.
New device discovered: TB2 .
Discovery stopped.
Available Bluetooth devices:
- TB2 : [c0:50:06:32:19:33]
Select a device to connect to (‘0’ to quit): 1
Connecting to TB2 …
Device TB2 disconnected unexpectedly.
Exiting…
Error number two:
guest@raspberrypi:~ $ sudo python3 example_ble_1.py
##################
BlueST Example
##################
Scanning Bluetooth devices…
Discovery started.
Traceback (most recent call last):
File “/usr/local/lib/python3.9/dist-packages/blue_st_sdk/manager.py”, line 313, in discover
self._scanner.scan(timeout_s)
File “/usr/local/lib/python3.9/dist-packages/bluepy/btle.py”, line 854, in scan
self.stop()
File “/usr/local/lib/python3.9/dist-packages/bluepy/btle.py”, line 803, in stop
self._mgmtCmd(self._cmd()+“end”)
File “/usr/local/lib/python3.9/dist-packages/bluepy/btle.py”, line 309, in _mgmtCmd
rsp = self._waitResp(‘mgmt’)
File “/usr/local/lib/python3.9/dist-packages/bluepy/btle.py”, line 362, in _waitResp
raise BTLEDisconnectError(“Device disconnected”, resp)
bluepy.btle.BTLEDisconnectError: Device disconnected
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File “/home/guest/example_ble_1.py”, line 312, in
main(sys.argv[1:])
File “/home/guest/example_ble_1.py”, line 189, in main
manager.discover(SCANNING_TIME_s)
File “/usr/local/lib/python3.9/dist-packages/blue_st_sdk/manager.py”, line 325, in discover
raise BlueSTInvalidOperationException(msg)
blue_st_sdk.utils.blue_st_exceptions.BlueSTInvalidOperationException:
Bluetooth scanning requires root privilege, so please run the script with “sudo”.
Matt_Mielke DigiKey Employee
3月26日
こんにちは @milan.vercammen さん
ALLMEMS1ファームウェアをSTEVAL-MKSBOX1V1に書き込んだ後、私もあなたの問題を再現できることをお知らせしたいと思います。デバイスがBlueST Python SDKによって発見されません。
しかしながら、あなたは次のように続けています。
「ファームウェアなし」とはどういう意味でしょうか?別のファームウェアのことを言っているのでしょうか? もしそうなら、どのファームウェアでしょうか?
milan.vercammen
3月26日
こんにちは @Matt_Mielke さん
私は2つのSTEVAL-MKSBOX1V1センサを持っており、そのうちの1つにはALLMEMS1ファームウェアを書き込みました。その結果、デバイスがBlueST Python SDKによって発見されないという問題が発生しています。もう1つのセンサにはALLMEMS1を書き込んでおらず、購入時のオリジナルのファームウェア(センサを購入したときに付属するもの)が入っています。そのセンサでは常に2つの出力メッセージのうちの1つが得られます。
Matt_Mielke DigiKey Employee
3月 28 日
こんにちは @milan.vercammen さん
お待ちいただき、ありがとうございます。
ALLMEMS1プロジェクトの最新バージョンとBlueST Python SDKの両方のソースコードを調査した結果、これら2つが互いに互換性がなくなったという結論に達しました。ALLMEMS1の機能パックはSTから継続的にアップデートされていますが、BlueST SDKは約5年間アップデートされていません。良いニュースとしては、まだ互換性がある機能パックのバージョン4.1.0をダウンロードできることです。私も試してみたところ、上記のBluetooth接続のテスト手順に示されているように、センサに正常に接続できました。
最新のファームウェアを書き込んだ際に、STEVAL-MKSBOX1V1がSDKによって認識されない理由は、そのアドバタイジングデータが次のように定義されているためです(プロジェクトソースコードの BLE_Manager.c
行2303~2334より)。
void updateAdvData()
{
/* Filling Manufacter Advertise data */
manuf_data[0 ] = 8U;
manuf_data[1 ] = 0x09U;
manuf_data[2 ] = (uint8_t)BLE_StackValue.BoardName[0];/* Complete Name */
manuf_data[3 ] = (uint8_t)BLE_StackValue.BoardName[1];
manuf_data[4 ] = (uint8_t)BLE_StackValue.BoardName[2];
manuf_data[5 ] = (uint8_t)BLE_StackValue.BoardName[3];
manuf_data[6 ] = (uint8_t)BLE_StackValue.BoardName[4];
manuf_data[7 ] = (uint8_t)BLE_StackValue.BoardName[5];
manuf_data[8 ] = (uint8_t)BLE_StackValue.BoardName[6];
manuf_data[9 ] = 15U;
manuf_data[10] = 0xFFU;
manuf_data[11] = 0x30U;/* STM Manufacter AD */
manuf_data[12] = 0x00U;
#ifdef BLE_MANAGER_SDKV2
manuf_data[13] = 0x02U;
#else /* BLE_MANAGER_SDKV2 */
manuf_data[13] = 0x01U;
#endif /* BLE_MANAGER_SDKV2 */
manuf_data[14] = BLE_MANAGER_USED_PLATFORM; /* BoardType */
manuf_data[15] = 0x00U;
manuf_data[16] = 0x00U;
manuf_data[17] = 0x00U;
manuf_data[18] = 0x00U;
manuf_data[19] = BLE_StackValue.BleMacAddress[5]; /* BLE MAC start */
manuf_data[20] = BLE_StackValue.BleMacAddress[4];
manuf_data[21] = BLE_StackValue.BleMacAddress[3];
manuf_data[22] = BLE_StackValue.BleMacAddress[2];
manuf_data[23] = BLE_StackValue.BleMacAddress[1];
manuf_data[24] = BLE_StackValue.BleMacAddress[0]; /* BLE MAC stop */
manuf_data[9:19]
のバイトは「ベンダー固有フィールド」を構成しており、SDKによればこのプロトコルに従って定義されるべきです。近いものの、アドバタイジングデータはもはやプロトコルに従っていません。実際、プロジェクトはBLE_Manager_Conf.hでBLE_MANAGER_SDKV2を定義しており、SDKの新しいバージョン(バージョン2)に準拠していることを示しています。しかし、BlueST SDKはblue_st_advertising_data_parser.pyでバージョン1のみがサポートされていると明示しています。
VERSION_PROTOCOL_SUPPORTED_MIN = 0x01
"""Minimum version protocol supported."""
VERSION_PROTOCOL_SUPPORTED_MAX = 0x01
"""Maximum version protocol supported."""
milan.vercammen
4月25日
こんにちは @Matt_Mielke さん
ご助力いただき本当にありがとうございます!別の用事が入ったため、このプロジェクトを一時中断していましたが、今再びマイク機能をRaspberry Piに接続しようとしています。しかし、この作業を試みたところ、次のようなエラーメッセージが表示されます。
guest@raspberrypi:~ $ sudo python3 mic_test.py
name ‘manager’ is not defined
Scanning Bluetooth devices…
Discovery started.
New device discovered: AM1V410.
Discovery stopped.
Traceback (most recent call last):
File “/home/guest/mic_test.py”, line 196, in
main()
File “/home/guest/mic_test.py”, line 113, in main
device = filtered_devices[0]
IndexError: list index out of range
何が問題なのかわかるでしょうか?なぜなら、私はこの分野の経験があまりありません。
Matt_Mielke DigiKey Employee
4月25日
こんにちは @milan.vercammen さん
ソースファイル mic_test.py
で、 DEVICE_NAME
を何に定義しましたか?
milan.vercammen
4月25日
最初はAM1V400だったのですが、それを見てすぐにAM1V410に変更しました。変更後は以下のとおりです。
guest@raspberrypi:~ $ sudo python3 mic_test.py
name ‘manager’ is not defined
Scanning Bluetooth devices…
Discovery started.
New device discovered: AM1V410.
Discovery stopped.
Connecting to device: AM1V410 (f1:af:81:8a:e3:ce)
Device AM1V410 connected.
Available features:
Gesture
Activity Recognition
Temperature
Temperature
Humidity
Pressure
Magnetometer
Gyroscope
Accelerometer
Traceback (most recent call last):
File “/home/guest/mic_test.py”, line 196, in
main()
File “/home/guest/mic_test.py”, line 132, in main
feature = filtered_features[0]
IndexError: list index out of range
Device AM1V410 disconnected.
Exception ignored in: <function Peripheral.del at 0x7656dbf8>
Traceback (most recent call last):
File “/usr/local/lib/python3.9/dist-packages/bluepy/btle.py”, line 630, in del
File “/usr/local/lib/python3.9/dist-packages/blue_st_sdk/node.py”, line 555, in disconnect
File “/usr/local/lib/python3.9/dist-packages/blue_st_sdk/node.py”, line 397, in _update_node_status
File “/usr/lib/python3.9/concurrent/futures/thread.py”, line 161, in submit
RuntimeError: cannot schedule new futures after shutdown
Matt_Mielke DigiKey Employee
4月25日
NameErrorが適切に処理されていないようですね…。私の方で再確認できるよう、mic_test.py
ファイル全体を投稿していただけないでしょうか?
milan.vercammen
4月26日
import os
import sys
import time
from blue_st_sdk.manager import Manager, ManagerListener
from blue_st_sdk.node import NodeListener
from blue_st_sdk.feature import FeatureListener
from blue_st_sdk.features.audio.adpcm.feature_audio_adpcm import FeatureAudioADPCM
from blue_st_sdk.features.audio.adpcm.feature_audio_adpcm_sync import FeatureAudioADPCMSync
from blue_st_sdk.utils.uuid_to_feature_map import UUIDToFeatureMap
from blue_st_sdk.utils.ble_node_definitions import FeatureCharacteristic
from feature_microphone import FeatureMicrophone
SCANNING_TIME_s = 5
MAX_NOTIFICATIONS = 15
DEVICE_NAME = “AM1V410”
class MyManagerListener(ManagerListener):
#
# This method is called whenever a discovery process starts or stops.
#
# @param manager Manager instance that starts/stops the process.
# @param enabled True if a new discovery starts, False otherwise.
#
def on_discovery_change(self, manager, enabled):
print('Discovery %s.' % ('started' if enabled else 'stopped'))
if not enabled:
print()
#
# This method is called whenever a new node is discovered.
#
# @param manager Manager instance that discovers the node.
# @param node New node discovered.
#
def on_node_discovered(self, manager, node):
print('New device discovered: %s.' % (node.get_name()))
class MyNodeListener(NodeListener):
#
# To be called whenever a node connects to a host.
#
# @param node Node that has connected to a host.
#
def on_connect(self, node):
print('Device %s connected.' % (node.get_name()))
#
# To be called whenever a node disconnects from a host.
#
# @param node Node that has disconnected from a host.
# @param unexpected True if the disconnection is unexpected, False otherwise
# (called by the user).
#
def on_disconnect(self, node, unexpected=False):
print('Device %s disconnected%s.' % \
(node.get_name(), ' unexpectedly' if unexpected else ''))
if unexpected:
# Exiting.
print('\nExiting...\n')
sys.exit(0)
class MyFeatureListener(FeatureListener):
_notifications = 0
"""Counting notifications to print only the desired ones."""
#
# To be called whenever the feature updates its data.
#
# @param feature Feature that has updated.
# @param sample Data extracted from the feature.
#
def on_update(self, feature, sample):
if self._notifications < MAX_NOTIFICATIONS:
self._notifications += 1
print(feature)
def main():
try:
# Creating Bluetooth Manager.
manager = Manager.instance()
manager_listener = MyManagerListener()
manager.add_listener(manager_listener)
# Append custom mic level feature into the BlueST library before discovery
mask_to_features_dic = FeatureCharacteristic.SENSOR_TILE_BOX_MASK_TO_FEATURE_DIC
mask_to_features_dic[0x04000000] = FeatureMicrophone
try:
Manager.add_features_to_node(0x06, mask_to_features_dic)
except Exception as e:
print(e)
# Synchronous discovery of Bluetooth devices.
print('Scanning Bluetooth devices...\n')
manager.discover(SCANNING_TIME_s)
# Getting discovered devices.
discovered_devices = manager.get_nodes()
if not discovered_devices:
raise Exception("No devices found.")
# Find the correct device name
filtered_devices = list(filter(lambda x: x.get_name() == DEVICE_NAME, discovered_devices))
# Only have the one device right now
device = filtered_devices[0]
print("Connecting to device: {} ({})\n".format(device.get_name(), device.get_tag()))
node_listener = MyNodeListener()
device.add_listener(node_listener)
if not device.connect():
print('Connection failed.\n')
raise Exception("Failed to connect to device {} ({}).".format(device.get_name(), device.get_tag()))
# Getting features.
features = device.get_features()
print("Available features:")
for each in features:
print(each.get_name())
# Get only the mic feature
filtered_features = list(filter(lambda x: x.get_name() == "Microphone", features))
feature = filtered_features[0]
print("Listening to {} feature...".format(feature.get_name()))
# Enabling notifications.
feature_listener = MyFeatureListener()
feature.add_listener(feature_listener)
device.enable_notifications(feature)
# Handling audio case (both audio features have to be enabled).
if isinstance(feature, FeatureAudioADPCM):
audio_sync_feature_listener = MyFeatureListener()
audio_sync_feature.add_listener(audio_sync_feature_listener)
device.enable_notifications(audio_sync_feature)
elif isinstance(feature, FeatureAudioADPCMSync):
audio_feature_listener = MyFeatureListener()
audio_feature.add_listener(audio_feature_listener)
device.enable_notifications(audio_feature)
# Getting notifications.
notifications = 0
start_time = time.time()
while notifications < MAX_NOTIFICATIONS:
if device.wait_for_notifications(0.05):
start_time = time.time()
notifications += 1
if time.time() > start_time + 10:
print("Timed out waiting for notifications.")
break
print("Shutting down...")
# Disabling notifications.
device.disable_notifications(feature)
feature.remove_listener(feature_listener)
# Handling audio case (both audio features have to be disabled).
if isinstance(feature, FeatureAudioADPCM):
device.disable_notifications(audio_sync_feature)
audio_sync_feature.remove_listener(audio_sync_feature_listener)
elif isinstance(feature, FeatureAudioADPCMSync):
device.disable_notifications(audio_feature)
audio_feature.remove_listener(audio_feature_listener)
# Shut everything down
device.remove_listener(node_listener)
device.disconnect()
manager.remove_listener(manager_listener)
except KeyboardInterrupt:
print("Program killed. Exiting...")
device.disable_notifications(feature)
feature.remove_listener(feature_listener)
device.remove_listener(node_listener)
device.disconnect()
manager.remove_listener(manager_listener)
sys.exit(0)
except SystemExit:
os._exit(0)
if name == “main”:
main()
milan.vercammen
4月26日
それとも @Matt_Mielke さん、ファイルがここにあります。
mic_test.py (6,7 KB)
Matt_Mielke DigiKey Employee
4月26日
ありがとうございます。コードには、そのエラー メッセージが表示されるような明らかな問題は見当たりません。私のローカル設定であなたに起こっている問題を再現する必要があります。
それまでの間、コードのこの部分を変更してもらえますか?
# Append custom mic level feature into the BlueST library before discovery
mask_to_features_dic = FeatureCharacteristic.SENSOR_TILE_BOX_MASK_TO_FEATURE_DIC
mask_to_features_dic[0x04000000] = FeatureMicrophone
try:
Manager.add_features_to_node(0x06, mask_to_features_dic)
except Exception as e:
print(e)
このコードと一致するようにしてください。
# Append custom mic level feature into the BlueST library before discovery
mask_to_features_dic = FeatureCharacteristic.SENSOR_TILE_BOX_MASK_TO_FEATURE_DIC
mask_to_features_dic[0x04000000] = FeatureMicrophone
Manager.add_features_to_node(0x06, mask_to_features_dic)
そのコードを実行した後の出力を投稿してもらえますか?
これで問題が解決するとは思っていませんが、エラーの原因についてより詳しい情報が得られるはずです。
milan.vercammen
4月29日
@Matt_Mielke さん、コードを調整した結果、このような出力が得られました。
guest@raspberrypi:~ $ sudo python3 mic_test.py
File “/home/guest/mic_test.py”, line 93
mask_to_features_dic = FeatureCharacteristic.SENSOR_TILE_BOX_MASK_TO_FEATURE_DIC
milan.vercammen
5月3日
こんにちは @Matt_Mielke さん。エラーは解消したと言いたかったのですが、マイク機能をRaspberry Piに追加するのにまだ問題が残っています。もしかしたら、これ以上進めることは不可能なのでしょうか?
これが現在の出力です。
Creating Bluetooth Manager
done
Scanning Bluetooth devices…
Discovery started.
New device discovered: AM1V410.
Discovery stopped.
[<blue_st_sdk.node.Node object at 0x762a52f8>]
Connecting to device: AM1V410 (ea:eb:d5:a3:cf:fd)
Device AM1V410 connected.
Available features:
Gesture
Activity Recognition
Temperature
Temperature
Humidity
Pressure
Magnetometer
Gyroscope
Accelerometer
Available features:
Gesture
Activity Recognition
Temperature
Temperature
Humidity
Pressure
Magnetometer
Gyroscope
Accelerometer
Microphone not in features
Listening to Temperature feature…
Temperature(1796): 26.0 C
Temperature(1832): 26.0 C
Temperature(1868): 26.0 C
Temperature(1904): 25.9 C
Temperature(1940): 25.9 C
Temperature(1976): 25.9 C
Temperature(2012): 25.9 C
Temperature(2048): 25.9 C
Temperature(2084): 25.9 C
Temperature(2120): 25.9 C
Temperature(2156): 25.8 C
Temperature(2192): 25.8 C
Temperature(2228): 25.8 C
Temperature(2264): 25.8 C
Temperature(2300): 25.8 C
Shutting down…
mic_test.py(7,2 KB)
feature_microphone.py(3,5 KB)
Matt_Mielke DigiKey Employee
5月3日
こんにちは @milan.vercammen さん
別の方法を試してみましょう。まず、マネージャモジュールがインストールされている場所を見つけましょう。
$ python -c "import blue_st_sdk.manager; print(blue_st_sdk.manager.__file__)"
次に、 manager.py
ファイルを開いて、 add_features_to_node()
関数の定義を見つけます。それは522行目にあるはずです。doc stringの後に、次のコメントが表示されるはずです。
# Example:
# # Adding a 'MyFeature' feature to a Nucleo device and mapping it to a
# # custom '0x10000000-0001-11e1-ac36-0002a5d5c51b' characteristic.
# mask_to_features_dic = {}
# mask_to_features_dic[0x10000000] = my_feature.MyFeature
# try:
# Manager.add_features_to_node(0x80, mask_to_features_dic)
# except BlueSTInvalidFeatureBitMaskException as e:
# print(e)
# Synchronous discovery of Bluetooth devices.
manager.discover(False, SCANNING_TIME_s)
最後の行をコメントアウトしてください。
# Example:
# # Adding a 'MyFeature' feature to a Nucleo device and mapping it to a
# # custom '0x10000000-0001-11e1-ac36-0002a5d5c51b' characteristic.
# mask_to_features_dic = {}
# mask_to_features_dic[0x10000000] = my_feature.MyFeature
# try:
# Manager.add_features_to_node(0x80, mask_to_features_dic)
# except BlueSTInvalidFeatureBitMaskException as e:
# print(e)
# Synchronous discovery of Bluetooth devices.
# manager.discover(False, SCANNING_TIME_s)
ファイルを保存して、元のmic_test.pyスクリプトを実行してみてください。
milan.vercammen
5月6日
こんにちは @Matt_Mielke さん
最後の行をコメントアウトした後、マイク機能に接続できるようになりました!
なぜこの行のコードが問題を引き起こしたのか、分るでしょうか?
Matt_Mielke DigiKey Employee
5月6日
良かったですね、 @milan.vercammen さん。その行は、開発者に関数の使用目的を知らせるためにコメントアウトされたサンプルの一部であるように思えます。これはBlueST SDKドキュメント1と2にほぼそのまま示されています。 おそらく、コードはリファクタリング中に変更されたのでしょうが、誰もそれ(リファクタリング後のコード)をテストしようとはしなかったのでしょう。
私はメインの記事にいくつかの編集を加えて、今後の読者にこの問題と前の問題について知らせることにします。以前にも述べましたが、このSDKはSTによって放置されたように見えますので、なにもなければこのまま(バグを)受け入れていく必要があるでしょう。これらの問題を明るみに出して頂いたあなたに感謝します!