IoTスタバボットを作って店員さんと話してみた

## 概要
 カップスピーカを作ってスタバのドリンク注文をボット化、注文履歴をSORACOM Harvestで収集、 Lagoon可視化して、IoT視える化したお話です。


## 完成品の紹介

<iframe width="560" height="315" src="https://www.youtube.com/embed/aLlfGAOWmrQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


スタバのメニューが有機ELディスプレイに表示され、レバースイッチのRightとLeftを操作しながら好きなドリンクを探します。
飲みたいドリンクが決まったらCenterボタンを押し決定します。店員さんに超音波センサの前に手をかざしてもらうあるいは、自分でボットをお辞儀させてレジ台を検知させるなどして、センサの測距値が一定の距離以下になるようにすると、注文音声のデータが再生されます。再生終了後、SORACOM Harvestに注文ドリンク名と数量データが送られます。注文履歴はSORACOM Lagoonで確認できます！


## 製作の動機
#### 1. 店員さんとの話のきっかけ

私は週6～8日、スタバに行きます。それだけ行っていると馴染みの店員さんとお話することもあるのですが、話の内容が似通ってきてしまったり、一方で気にはなってるけどあまり話したことない店員さんもいるのですが、話してみたいけど何て話しかけたらいいんだろうって思ったりすることがあります。いつもと違う話のきっかけ…。何かほしいなと思いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/576644ad-8e91-3747-ca26-b8c8b163ff68.png)



#### 2.資源の有効活用

普段はMyタンブラーを持ち歩いているのですが、たまたま車中に置き忘れることがあり、いつもと違ってその日は紙カップ。飲み終わった後、ほんのちょっと、なんか捨てるのもったいないな、有効活用出来ないかなと感じました。これ積極的に工作に使えないかな？と思いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/12bdcef9-ed6c-6947-430f-c526a1c03f12.png)
(左、真ん中：いつも私が使ってる愛用タンブラー、右：Lサイズのスタバ紙カップ）

#### 3.健康管理のはじめの一歩

ホワイトモカ、ホワイトホットチョコレート、チャイティーラテ…。スタバには甘くて魅惑的なドリンクがいっぱい。甘い系ドリンクをプログラミングの勉強するとき飲むと一時的にめっちゃ脳が活性化するので、私の作業効率は爆上がりするのですが、長期的にみると、もうちょっとシンプルな紅茶系とか糖分の少ない系のものもバランスよく飲んだ方が健康的かなと思ったりします。どのドリンクをどのくらい飲んでいるかまずはちゃんと客観的に把握したいなと感じました。。今、私は、Reward集めるのにスタバのアプリ使ってるのですが、私のアプリでは注文履歴は表示されません。無いものは創る！注文履歴のグラフ可視化機能も付けようと思いました。


## 目標

健康と地球環境に気を配り、さらに遊び心を大切にし、毎日のスタバライフをいつも以上に楽しい気持ちにさせてくれる！を目標に以下を実現するMyスタバボット開発(個人プロジェクト)を行うことにしました。

① 私の代わりに音声で注文をし、店員さんとの話題のきっかけを作ってくれるボット*
② 本来捨てられるはずだったカップを有効活用した地球環境に配慮したちょっぴりエコなボット
③ 注文履歴を視える化し、糖分を取り過ぎていないか客観的にチェックできるようにするボット

※時間帯によっては、スピーカ注文すると他のお客さんの迷惑になったりするかもしれないし、
　毎日この機能使うとさすがに飽きられちゃうので、マナーモードも実装しておきたいと思います。　

頭の中のアイデアを具現化すべく開発に取り組むことにしました。



## 全体構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/eddd4f61-5748-9c17-0fb0-5d071a7ba0fe.png)

動作の概略は以下の通りです。
① 緑のボタンはGPIO3(SCL)に繋がっているので押すとラズパイに電源投入されます。
　ラズパイ起動時に本アプリの実行します。(cron)
② I2C接続したOLEDにドリンク名を表示します。
③ レバースイッチのRightとLeftで注文名切替、Centerで決定を行います。
④ 超音波センサで測距します。手をかざして測距値が閾値以下を検知すると
⑤ Speaker Bonnetに接続したスピーカから注文音声が再生されます。音声出力はI2Sを利用しました。
⑥ 再生中、ピカっとかっこよくシリアルLEDを光らせます。
⑦ SORACOM Airでプラットフォームと接続、注文名と数量のJSONデータを送ります。
⑧ Harvestでデータ収集＆Lagoon可視化します。ユーザがPC、タブレットで注文履歴を確認できます。
⑨ 一連の注文が終わったら、赤のボタンを押してシャットダウンします。

## 準備
#### a) Speaker bonnet の設定
公式ページ(https://learn.adafruit.com/adafruit-speaker-bonnet-for-raspberry-pi/raspberry-pi-usage)
を参考に以下のコマンドを実行
```bash
$ curl -sS https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/i2samp.sh | bash
```

#### b) SORACOM Airの設定
公式ページ(https://users.soracom.io/ja-jp/guides/devices/general/raspberry-pi-dongle/）
を参考に以下のコマンドを実行
```bash
curl -O https://soracom-files.s3.amazonaws.com/setup_air.sh
$ sudo bash setup_air.sh
```
実行結果
```bash
$ sudo bash setup_air.sh
.
.
.
Adding udev rules for modem detection.
ok.

--- 3. Connect
Found un-initilized modem. Trying to initialize it ...
Now you are all set.

Tips:
 - When you plug your usb-modem, it will automatically connect.
 - If you want to disconnect manually or connect again, you can use 'sudo ifdown wwan0' / 'sudo ifup wwan0' commands.
 - Or you can just execute 'sudo wvdial'.
```
接続確認
```bash
ifconfig ppp0
ppp0      Link encap:Point-to-Point Protocol
          inet addr:10.xxx.xxx.xxx  P-t-P:10.64.64.64  Mask:255.255.255.255
          UP POINTOPOINT RUNNING NOARP MULTICAST  MTU:1500  Metric:1
          RX packets:133 errors:0 dropped:0 overruns:0 frame:0
          TX packets:134 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:3
          RX bytes:2092 (2.0 KiB)  TX bytes:4039 (3.9 KiB)
```
ダッシュボードを確認。使用中オンラインの表示が出てます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/c570915a-7be6-a9cc-07c0-94677051dca5.png)




## 全体プログラム 
```python
import RPi.GPIO as GPIO
import time
import sys
import wiringpi as pi
import os
import subprocess
import shlex
import glob

from board import I2C
from adafruit_ssd1306 import SSD1306_I2C
from PIL import Image, ImageDraw, ImageFont
import board
import neopixel

import requests
import json

dir = "/home/pi/order_bot/wav_starbucks/*"
path = glob.glob(dir)
order_num=len(path)
print(path[0])

SW_L = 22
SW_R = 23
SW_C = 4
TRIG_PIN = 17
ECHO_PIN = 27

SILENT_PIN = 13

GPIO.setwarnings(False) 
GPIO.setmode(GPIO.BCM)
GPIO.setup([SW_L, SW_C, SW_R], GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(SILENT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

FONT_SANS_13 = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
FONT_IPAGUI_13 = ImageFont.truetype("/usr/share/fonts/truetype/ipafont-nonfree-uigothic/ipagui.ttf", 12)

pi.wiringPiSetupGpio()

pi.pinMode( TRIG_PIN, pi.OUTPUT )
pi.pinMode( ECHO_PIN, pi.INPUT )
pi.digitalWrite( TRIG_PIN, pi.LOW )

def measure(measure_flag:int):
    if measure_flag == 1:
        
        pi.digitalWrite( TRIG_PIN, pi.HIGH )
        time.sleep(0.00001)
        pi.digitalWrite( TRIG_PIN, pi.LOW )
        while ( pi.digitalRead( ECHO_PIN ) == pi.LOW ):
            sigoff = time.time()
        while ( pi.digitalRead( ECHO_PIN ) == 1 ):
            sigon = time.time()
        dist = ( sigon - sigoff ) * 17000

    else:
        dist = 100000
    return dist

def display_order(order:str, prev_order:str, dist:int, silent_mode:int):
      
    if order != prev_order:
                
        # Create display instance
        display = SSD1306_I2C(128, 64, I2C(), addr=0x3C)

        #clear_display
        display.fill(0)
        display.show()
        
        # Prepare image
        img = Image.new("1", (display.width, display.height))
        draw = ImageDraw.Draw(img)
        draw.text((0, 0), "starbucks bot2", font=FONT_SANS_13, fill=1)
        draw.text((0, 20), order, font=FONT_IPAGUI_13, fill=1)
        draw.text((0, 40), "dist:%.1f"%dist, font=FONT_IPAGUI_13, fill=1)
        print(silent_mode)
        if silent_mode == 1:
            draw.text((60, 40), "Mute",font=FONT_IPAGUI_13, fill=1)

        # Show image
        display.image(img)
        display.show()
    prev_order = order

def init_led():
    pixels = neopixel.NeoPixel( board.D12 , 14) 
    for i in range(14):
        pixels[i] = (0, 0, 32)
        time.sleep(0.2)
        pixels.fill(0)
    time.sleep(0.2)
    pixels.fill(0)
    
    command = "sudo aplay %s"%("/home/pi/order_bot/mp3_sample/決定ボタンを押す1.wav")
    subprocess.Popen(shlex.split(command)) 
    
def lightup_led():
    pixels = neopixel.NeoPixel( board.D12 , 14)    
    pixels[0] = (32, 0, 0)
    pixels[1] = (0, 32, 0)
    pixels[2] = (0, 0, 32)
    pixels[3] = (32, 32, 0)
    pixels[4] = (32, 0, 32)
    pixels[5] = (0, 32, 32)
    pixels[6] = (32, 32, 32)
    pixels[7] = (32, 0, 0)
    pixels[8] = (0, 32, 0)
    pixels[9] = (0, 0, 32)
    pixels[10] = (32, 32, 0)
    pixels[11] = (32, 0, 32)
    pixels[12] = (0, 32, 32)
    pixels[13] = (32, 32, 32)    
    
    pixels.show()
    time.sleep(4)
    
    pixels.fill(0)   
    time.sleep(0.2)

def lightoff_led():
    pixels = neopixel.NeoPixel(board.D12 , 14 )
    pixels.fill(0)

def ak20_isexist():
    command2 = "sudo lsusb -d 15eb:7d0e"
    ak20= subprocess.run(shlex.split(command2),encoding="utf-8", stdout=subprocess.PIPE)  
    if ak20.returncode == 0:
        exist = True
    else:
        exist = False
        
    return exist

def main():
    count = 0
    order =""
    prev_order =""
    measure_flag = 0
    dist = -1
    silent_mode = 0

    init_led()

    while True:
        try:
            if (GPIO.input(SILENT_PIN) == GPIO.LOW):
                 time.sleep(0.02)
                 silent_mode = 1
 
            if (GPIO.input(SW_R) == GPIO.HIGH):
                time.sleep(0.02)
                
                if count < order_num-1:
                    count = count + 1
                else:
                    count = 0
                
                while (GPIO.input(SW_R) == GPIO.HIGH):
                    pass
                order = os.path.splitext(os.path.basename(path[count]))[0]
                print(count, order)
                
                display_order(order, prev_order, dist, silent_mode)
                
                if silent_mode == 0:
                    command = "sudo aplay %s"%("/home/pi/order_bot/mp3_sample/決定ボタンを押す40.wav")
                    subprocess.Popen(shlex.split(command))
                
            if (GPIO.input(SW_L) == GPIO.HIGH):
                # chattering
                time.sleep(0.02)

                if count > 0:
                    count = count - 1
                else:
                    count = order_num -1
                while (GPIO.input(SW_L) == GPIO.HIGH):
                    pass
                
                order = os.path.splitext(os.path.basename(path[count]))[0]
                print(count, order)
                display_order(order, prev_order, dist, silent_mode)
                
                if silent_mode == 0:
                    command = "sudo aplay %s"%("/home/pi/order_bot/mp3_sample/決定ボタンを押す40.wav")
                    subprocess.Popen(shlex.split(command)) 


            if (GPIO.input(SW_C) == GPIO.HIGH):
                time.sleep(0.02)
                
                while (GPIO.input(SW_C) == GPIO.HIGH):
                    pass
                measure_flag = 1
                print("setted!", measure_flag)
                
                if silent_mode == 0:
                    command = "sudo aplay %s"%("/home/pi/order_bot/mp3_sample/決定ボタンを押す41.wav")
                    subprocess.Popen(shlex.split(command))           
               
            if measure_flag == 1:
                dist = measure(measure_flag)
                display_order(order,prev_order,  dist, silent_mode)
                
                if dist > 0 and dist < 10:
                
                    print("enter")
                    order = os.path.splitext(os.path.basename(path[count]))[0]
                    print(count, order)
                    
                    if silent_mode == 0:
                        command = "sudo aplay %s"%(path[count])
                        subprocess.Popen(shlex.split(command))
                    lightup_led()
                    
                    if ak20_isexist():
                        payload = {"message": "Button Pressed", order:1}
                        requests.post("http://harvest.soracom.io", data=json.dumps(payload))
                    
                    #flag_reset
                    measure_flag = 0
                    silent_mode = 0
                    
                time.sleep(0.3)
                
        except OSError:
            print("OS_error")
        
if __name__ == "__main__":
    main()          
```

#### 注文履歴データ確認(SORACOM Harvest)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/29f618eb-f819-eba5-8f3c-84f255d64473.png)



#### 注文履歴可視化画面(SORACOM Lagoon)

公式ページ(https://soracom.jp/recipes_index/5413/#SORACOM_Lagoon)を参考にポチポチとクリックしていき、以下の画面が出来ました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217319/1811534f-27e1-f300-cc9b-d732b83afaf5.png)


## 感想
7月末に製作開始して、今日は、I2Sで音声合成試してみよう、今日はSerial LED単体をカラフルに光らせてみよう、みたいな感じで最初はすごく小さな単位で試行しました。エラーが出たら、一つずつ原因調べて再トライ！公式ページ見たり、ググったりして色々調べました！一つの機能が動くだけでも面白い、次第にちょっとずつ機能を組み合わせてより複雑なことができるように、関数化し、手を動かしながら実際に動作を見てを繰り返し、どんどんコードを書きました。時間はかかるけど、用意されたハンズオンとはまた違った面白さがあるなと思ったりもしました。最初分からなかったことも段々わかるようになり、着手から4か月で自分の頭の中のふわっとした考えが形になりました。これはとても楽しい作業でした。

また、めっちゃドキドキしましたが、お客さんの少ない平日の閉店前の時間帯、勇気を振り絞って、最終的に自分が作ったMyスタバボットを店員さんに見て頂きました。コメント頂けたことは本当すごく幸せだなと思いました。話のきっかけにもすごくなりました。とても暖かなコメントを頂いたり、驚きなどの反応などもあり、また話のきっかけにもすごくなりました。。

作りたいものまだまだいっぱいあるので、引き続き、手を動かしながら、頭の中のアイデアをカタチにしていきたいなと思ってます。強強のエンジニア達に比べたら、まだまだひよっこの趣味レベルの工作かもですが、一歩ずつ歩みを進め、精進していきたいと思いますので、どうか暖かい目で見守って頂けるとありがたく思います。至らないところもあるかもですが、何卒、よろしくお願い致します！


## 今後の課題
SORACOM Harvestの保存期間のデフォルトが40日、延長しても731日らしい。
(https://users.soracom.io/ja-jp/docs/harvest/extend/）
プログラミングの学習、資格試験の勉強、楽譜作成などで私、5年くらいほぼ毎日スタバ通ってて、これからもスタバには長く通うと思います。なので、保存期間、もっと長くしたいなと感じてます。たとえばSORACOM AIRとAWSを併用する？あるいは、ソラコムサンタにお願いしたら叶うかな？色々調べてみようかな。。引き続き改良していこうかなと考えております。