12
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

オープンストリームAdvent Calendar 2018

Day 25

SORACOM LTE-M ButtonでIoTクリスマスツリー

Last updated at Posted at 2018-12-24

みんさん、こんにちは。

前回、「クリスマスツリーに音楽に合わせて光るLEDを装飾をしてみた」でクリスマスツリーを装飾しました。その際にLEDをON/OFFボタンでコントロールできないかと考えたいた時に、先月開催されたSORACOM Technology Camp 2018の会場で衝動買いしたSORACOM LTE-M Buttonを発見。「これで、コントロールできるのでは?」と思ったのでやってみました。

アクションの仕様

仕様を決めます。ボタンには3つのアクションが使えるので、それぞれのアクションを以下のようにしました。

ボタンアクション ツリー側のアクション
シングルクリック 音楽再生/一時停止
ダブルクリック   LEDモード切り替え(常灯、ビジュアライザ)
長押し   LED消灯、音楽の停止

構成図

全体の流れはこんな感じです。

diaglam.png

作ってみる

では、以下のステップにそって解説していきます。

  1. AWS IoTの準備
  2. Raspberry Piの準備
  3. Arduinoの準備
  4. LTE-Mボタンから呼ばれるLambda関数を作成
  5. LTE-Mボタンの登録
  6. AWS IoT 1-Click のプロジェクトを作成
  7. いいや!限界だ押すね!今だッ!

AWS IoTの準備

まずは、Raspberry PiAWS IoTに登録して、MQTTのやり取りができるようにします。

以下を参考にAWS IoTRaspberry Piを登録します。
[AWS IoT Device SDK for Pythonを使ってRaspberryPiとAWS IoTをつないでみる]
(https://dev.classmethod.jp/hardware/raspberrypi/aws-iot-device-sdk-for-python-raspberrypi-aws-iot/)

証明書のダウンロード、ポリシーのアタッチ、エンドポイントの確認までできたらAWS IoT側の準備は完了です。

Raspberry Piの準備

Raspberry Piでは音楽ファイルの再生/停止AWS IoTからのPublishされるTopicSubscribeして、Arduinoにシリアル通信でLEDのコントロールをするプログラムを用意します。

環境

  • RaspberryPi 3 Model B GNU/Linux 8.0 (jessie)
  • Python 3.6.7(3.7系だとライブラリのインストールがうまく行かなかったので3.6系にしました)

証明書を配置

作業フォルダにcertディレクトリを作成し、AWS IoTでダウンロードした証明書を配置します。

.
└── iot_xmas_tree
    └── cert
        ├── *********-certificate.pem.crt
        ├── *********-private.pem.key
        └── rootCA.pemcd 

ライブラリのインストール

使用するライブラリのインストールをします。

ライブラリ 使用用途
aws-iot-device-sdk-python v1.4.0 AWS IoTとの接続
omxplayer-wrapper omxplayerをPythonからコントロールするラッパーモジュール
pyserial Arduino間のシリアル通信で使用
sudo apt-get update && sudo apt-get install -y libdbus-1{,-dev}
pip install pyserial AWSIoTPythonSDK omxplayer-wrapper

プログラム

AWS IoT接続情報を設定して、先ほど作成したcertディレクトリと同階層に配置します。

xmas_tree.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from logging import basicConfig, getLogger, INFO
from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
from omxplayer.player import OMXPlayer
from pathlib import Path
import enum
import time
import json
import serial

# AWSIoT接続情報
CLIENT_ID = 'myClientID'
ENDPOINT = '***************.iot.ap-northeast-1.amazonaws.com'
PORT = 8883
ROOT_CA = './cert/rootCA.pem'
PRIVATE_KEY = './cert/*******-private.pem.key'
CERTIFICATE = './cert/*******-certificate.pem.crt'
TOPIC = 'LTEButtonTopic/action'

plaing_music = None
music_path = None
player = None
ser = None
current_led_mode = None

class LED(enum.Enum):
    ON = 'O'
    VISUALIZER = 'V'
    OFF = 'F'

def main():
    # AWSIoTと接続
    myMQTTClient = AWSIoTMQTTClient(CLIENT_ID)
    myMQTTClient.configureEndpoint(ENDPOINT, PORT)
    myMQTTClient.configureCredentials(ROOT_CA, PRIVATE_KEY, CERTIFICATE)
    myMQTTClient.configureOfflinePublishQueueing(-1)
    myMQTTClient.configureDrainingFrequency(2)
    myMQTTClient.configureConnectDisconnectTimeout(10)
    myMQTTClient.configureMQTTOperationTimeout(5)
    myMQTTClient.connect()
    while True:
        myMQTTClient.subscribe(TOPIC, 1, subscribe_callback)
        time.sleep(1)

    current_led_mode = LED.ON.value

# AWSIoTからメッセージ受診時のコールバック
def subscribe_callback(client, userdata, message):
    print('\n\n--------------')
    logger.info('Received a new message: ')
    logger.info(message.payload)

    json_data = json.loads(message.payload)

    try:
        click_type = json_data['click_type']
        music_file = json_data['music_file']
    except KeyError:
        logger.error('keys could not be found')
        print('--------------\n\n')
        return
    global music_path
    music_path = Path(f'../../Music/{music_file}')
    if music_path.is_file():
        # シングルクリック時の処理
        if click_type == "SINGLE":
            play_pause_music(music_file)
        # ダブルクリック時の処理
        elif click_type == "DOUBLE":
            global current_led_mode
            led_mode = LED.ON.value if current_led_mode == LED.VISUALIZER.value else LED.VISUALIZER.value
            send_data_to_arduino(led_mode)
            current_led_mode = led_mode
        # 長押し時の処理
        elif click_type == "LONG":
            stop_all()
        else:
            logger.info('None')
    else:
        logger.error(f'{music_file} could not be found')

    print('--------------\n\n')

# Arduinoへシリアルデータを送る
def send_data_to_arduino(act_code):
    try:
        global ser
        if ser is None:
            ser = serial.Serial('/dev/ttyACM0', 9600, timeout = None)
            time.sleep(3)
        ser.write(act_code.encode())
        logger.info(f'Sucsses to send data to Arduino:{act_code}')
    except serial.serialutil.SerialException:
        logger.error('Failed to send data to Arduino')
        logger.error('Check the connection between Arduino and Raspberry pi by type /dev/ttyACM0')

# プレイヤーのコントロール(再生 or 一時停止)
def play_pause_music(music_file):
    global player
    global music_path
    global plaing_music

    if  player is None:
        player = OMXPlayer(music_path)
        plaing_music = music_file
        player.exitEvent += lambda p, e: clean_player()
        logger.info(f'♪♪♪ Start music: {plaing_music} ♪♪♪')
    elif music_file != plaing_music:
        logger.info(f'chenge music {plaing_music} → {music_file}')
        player.load(music_path)
        plaing_music = music_file
        logger.info(f'♪♪♪ Start music: {plaing_music} ♪♪♪')
    else:
        if player.is_playing():
            player.pause()
            logger.info(f'♪♪♪ Pause music: {plaing_music} ♪♪♪')
        else:
            player.play()
            logger.info(f'♪♪♪ Play music: {plaing_music} ♪♪♪')

def stop_all():
    global player
    if not player is None:
        player.stop()
        clean_player()

    send_data_to_arduino(LED.OFF.value)
    global current_led_mode
    current_led_mode = LED.VISUALIZER.value

def clean_player():
    global player
    player = None

if __name__ == '__main__':
    basicConfig(level=INFO)
    logger = getLogger(__name__)
    main()

音楽ファイルの再生確認

Pythonの対話モードで、音楽ファイルが再生されることを確認します。

確認する前にMusicフォルダに音楽ファイルを配置して、ヘッドフォンジャックにイヤホンかスピーカーを接続してください。また、Raspberry Piの音声出力は、デフォルト設定ではHDMIケーブルかヘッドフォンジャックどちらか自動で判別されて再生されるようになっています。ヘッドフォンジャックが選択されていることを確認してください。

$ python
Python 3.6.7 (default, Dec 13 2018, 04:35:31) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from omxplayer.player import OMXPlayer
>>> from pathlib import Path
>>> music_path = Path('../../Music/sample.mp3') # 音楽ファイルのパス
>>> player = OMXPlayer(music_path) #再生開始
>>> player.pause() # 一時停止
>>> player.play() # 再生
>>> player.stop() # 停止
>>> quit()

音楽の再生/停止が確認できたらOKです。

MQTTの動作確認

AWS IoTのコンソールからトピックに発行してMQTTの動作確認をします。

プログラムを起動します。

$ python xmas_tree.py
INFO:AWSIoTPythonSDK.core.protocol.mqtt_core:MqttCore initialized
INFO:AWSIoTPythonSDK.core.protocol.mqtt_core:Client id: myClientID
INFO:AWSIoTPythonSDK.core.protocol.mqtt_core:Protocol version: MQTTv3.1.1
INFO:AWSIoTPythonSDK.core.protocol.mqtt_core:Performing sync subscribe...

AWS IoTのコンソールからトピックに発行ボタンを押します。MQTTの接続確認だけを行いたいので、メッセージはデフォルトの物を使用します。

DropShadow ~ topic.png

Raspberry Pi側でメッセージが受信ができたらMQTTの動作確認はOKです。


--------------
INFO:__main__:Received a new message: 
INFO:__main__:b'{\n  "message": "Hello from AWS IoT console"\n}'
ERROR:__main__:keys could not be found
--------------

Arduinoの準備

前回の投稿で作成したスケッチにシリアル通信の箇所を追記したものです。以下のスケッチをArduinoに書き込みます。

スケッチ

sketch_christmas_tree

#include "FastLED.h"

// LEDの設定
#define LED_PIN     6
#define NUM_LEDS    60 // LEDの数
#define BRIGHTNESS  64
#define LED_TYPE    WS2811
#define COLOR_ORDER GRB
#define UPDATES_PER_SECOND 100
#define REACT_LEVEL 512L // 音の反応レベル値

CRGB leds[NUM_LEDS];

// オーディオ入力の設定
int strobe = 4;
int reset = 5;
int audio1 = A0;
int audio2 = A1;
int left[7];
int right[7];
int band;
int audio_input = 0;
int freq = 0;

// ビジュアライザの変数
int midway = NUM_LEDS / 2; // 真ん中のLED
int loop_max = 0;
int k = 255;
int decay = 0;
int decay_check = 0;
long pre_react = 0;
long react = 0;
long post_react = 0;
int wheel_speed = 2;

// LEDのモード
bool is_visualizer_mode = false;
char led_mode = 'O';

void setup()
{
  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);

  // スペクトルの設定
  pinMode(audio1, INPUT);
  pinMode(audio2, INPUT);
  pinMode(strobe, OUTPUT);
  pinMode(reset, OUTPUT);
  digitalWrite(reset, LOW);
  digitalWrite(strobe, HIGH);

  // LEDの明るさを調整
  delay( 3000 );
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip );
  FastLED.setBrightness(  BRIGHTNESS );

  // LEDの初期化
  initLED();

  // シリアル入力にの設定
  Serial.begin(115200);
  Serial.println("\nListening from Raspberry Pi...");

  Serial.begin(9600);
  Serial.println("\nListening from Raspberry Pi...");
}

// 虹色の作成
// 参考 https://github.com/NeverPlayLegit/Rainbow-Fader-FastLED/blob/master/rainbow.ino
CRGB Scroll(int pos) {
  CRGB color (0, 0, 0);
  if (pos < 85) {
    color.g = 0;
    color.r = ((float)pos / 85.0f) * 255.0f;
    color.b = 255 - color.r;
  } else if (pos < 170) {
    color.g = ((float)(pos - 85) / 85.0f) * 255.0f;
    color.r = 255 - color.g;
    color.b = 0;
  } else if (pos < 256) {
    color.b = ((float)(pos - 170) / 85.0f) * 255.0f;
    color.g = 255 - color.b;
    color.r = 1;
  }
  return color;
}

// MSGEQ7(スペクトラムアナライザ)から値を読み込む
void readMSGEQ7()
{
  digitalWrite(reset, HIGH);
  digitalWrite(reset, LOW);
  for (band = 0; band < 7; band++)
  {
    digitalWrite(strobe, LOW);
    delayMicroseconds(30); //
    left[band] = analogRead(audio1);
    right[band] = analogRead(audio2);
    digitalWrite(strobe, HIGH);
  }
}

// 真ん中のLEDから両端に向けてビジュアライズする
void doubleVisualizer()
{
  for (int i = NUM_LEDS - 1; i >= midway; i--) {
    if (i < react + midway) {
      leds[i] = Scroll((i * 256 / 50 + k) % 256);
      leds[(midway - i) + midway] = Scroll((i * 256 / 50 + k) % 256);
    }
    else
      leds[i] = CRGB(0, 0, 0);
    leds[midway - react] = CRGB(0, 0, 0);
  }
  FastLED.show();
}

// オーディオ信号からLEDを光らせるレベルを算出
void convertDouble()
{
  if (left[freq] > right[freq])
    audio_input = left[freq];
  else
    audio_input = right[freq];

  if (audio_input > 80)
  {
    pre_react = ((long)midway * (long)audio_input) / REACT_LEVEL;

    if (pre_react > react)
      react = pre_react;

    Serial.print(audio_input);
    Serial.print(" -> ");
    Serial.println(pre_react);
  }
}

void doubleLevel()
{
  readMSGEQ7();

  convertDouble();

  doubleVisualizer();
  k = k - wheel_speed;
  if (k < 0)
    k = 255;

  decay_check++;
  if (decay_check > decay)
  {
    decay_check = 0;
    if (react > 0)
      react--;
  }
}

void initLED()
{
  for (int i = 0; i < NUM_LEDS; i++)
    leds[i] = CRGB::Black;
  FastLED.show();
}

void loop()
{
    // シリアル通信
  if (Serial.available() > 0)
  {
    initLED();
    led_mode = Serial.read();
  }

  if (led_mode == 'O')
  {
    for (int i = 0; i < NUM_LEDS; i++)
      leds[i] = CRGB::Red;
    FastLED.show();
  }
  else if (led_mode == 'V')
  {
    doubleLevel();
  }
  else if (led_mode == 'F')
  {
    for (int i = 0; i < NUM_LEDS; i++)
      leds[i] = CRGB::Black;
    FastLED.show();
  }
}

書き込みが終わったら、Raspberry Piと、LEDテープを接続します。
LEDテープの接続の方法は前回の投稿を参照。

IMG_1357.jpg

シリアル通信の確認

Raspberry Pi側からPythonの対話モードで、シリアル通信ができるか確認します。

シリアルポートにアクセスできるようにttyACM0にアクセス許可をあげます。
対話モードで行うとアクセス許可がなくても動きますが、実行モード時はアクセス許可がないと動きません。ここハマりポイントです。

sudo usermod -a -G dialout <username>
sudo chmod a+rw /dev/ttyACM0

Pythonの対話モードで実行

$ python
Python 3.6.7 (default, Dec 13 2018, 04:35:31) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import serial
>>> ser = serial.Serial('/dev/ttyACM0', 9600, timeout = None)
>>> ser.write('F'.encode())
1
>>> ser.write('O'.encode())
1
>>> quit()

LEDテープのチカチカが確認できたらOKです。

LTE-Mボタンから呼ばれるLambda関数を作成

Lambda関数を作成します。この関数はクリックイベントのタイプと再生する音楽ファイル名をAWS IoTへpublishする関数です。

Lambda関数を作成

Python3.6で作成しました。ロールはAWS IoTにアクセスするため、AWSIoTFullAccessポリシーをアタッチされたロールにします。

DropShadow ~ Lambda_make.png

関数コード

lambda_function

# coding: utf-8

import json
import boto3

iot = boto3.client('iot-data')

def lambda_handler(event, context):
    topic = 'LTEButtonTopic/action'
    payload = {
        "click_type": event['deviceEvent']['buttonClicked']['clickType'],
        "music_file": event['placementInfo']['attributes']['musicFile']
    }
    
    try:
        iot.publish(
            topic=topic,
            qos=0,
            payload=json.dumps(payload, ensure_ascii=False)
        )

        return "Succeeeded."
    
    except Exception as e:
        print(e)
        return "Failed."

テスト

Lambdaコンソールのテストイベント設定からテストイベントを作成します。

DropShadow___test_event_16_01_32.png

以下はクリックイベントのタイプDOUBLEと音楽ファイル名sample_music.mp3を保持したテストイベントです。

DOUBLE
{
  "deviceInfo": {
    "deviceId": "070GGXXXXXXXXXX",
    "type": "button",
    "remainingLife": 99.3,
    "attributes": {
      "projectRegion": "ap-northeast-1",
      "projectName": "sample-project",
      "placementName": "Sample-Placement",
      "deviceTemplateName": "SampleRequest"
    }
  },
  "deviceEvent": {
    "buttonClicked": {
      "clickType": "DOUBLE",
      "reportedTime": "2018-12-17T16:48:33.804Z"
    }
  },
  "placementInfo": {
    "projectName": "Sample-Project",
    "placementName": "Sample-Placement",
    "attributes": {
      "musicFile": "sample_music.mp3"
    },
    "devices": {
      "Sample-Request": "070GGXXXXXXXXXX"
    }
  }
}
DropShadow ~ test_event.png

Raspberry Piのプログラムを起動して、テスト押します。

DropShadow ~ スクリーンショット_2018-12-23_12_47_49.png

Raspberry Pi側でメッセージが受信ができたら動作確認はOKです。

INFO:AWSIoTPythonSDK.core.protocol.mqtt_core:Performing sync subscribe...
INFO:AWSIoTPythonSDK.core.protocol.mqtt_core:Performing sync subscribe...


--------------
INFO:__main__:Received a new message: 
INFO:__main__:b'{"click_type": "DOUBLE", "music_file": "sample_music.mp3"}'
ERROR:__main__:sample_music.mp3 could not be found
--------------

LTE-Mボタンの登録

AWS IoT 1-Clickのコンソールから登録します。ボタンの後ろに記載されてるDSNを入力するだけです。簡単ですね。
DropShadow ~ スクリーンショット 2018-12-22 16.44.35.png

登録したら、ボタンの有効にします。

DropShadow ~ スクリーンショット_2018-12-22_16_50_15.png

AWS IoT 1-Click のプロジェクトを作成

AWS IoT 1-Clickのコンソールからプロジェクトを作成します。

1-Click.png 1-Click-info.png

デバイステンプレートを作成します。アクションをLambda関数を選択にしてLambda関数は作成した関数を指定します。

1-Click-tmp.png

続けて、プレイスメントを作成します。登録したボタンをデバイスに指定して、属性には再生したい音楽ファイル名を指定します。

1-Click-plc.png

ボタンとプロジェクトが紐づいたら、AWS IoT 1-Click の準備は完了です。

1-Click-chk.png

いいや!限界だ押すね!今だッ!

シングルクリック:音楽再生/一時停止

スピーカーからノイズが。。

ダブルクリック:LEDモード切り替え(常灯、ビジュアライザ)

長押し:LED消灯、音楽の停止

最後に

初めてのSORACOM LTE-M Buttonでした。ボタンを使用するまでが、とても簡単でしたのでプログラムの方に集中できました。LTEの通信網を使用しているのでWiFi環境がないところでも使えるというのは使用シーンが広がりますね。面白い使い方を思いついたら、また投稿したいと思います。

参考

[AWS IoT Device SDK for Pythonを使ってRaspberryPiとAWS IoTをつないでみる]
(https://dev.classmethod.jp/hardware/raspberrypi/aws-iot-device-sdk-for-python-raspberrypi-aws-iot/)
AWS IoT Device SDK for Pythonのサンプル
pyserialの公式ドキュメント
Pythonでシリアル通信
omxplayer-wrapperの公式ドキュメント

12
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?