LoginSignup
4

More than 1 year has passed since last update.

わんこそば自動お代わり機でコロナ禍の飲食業界を救いたい

Last updated at Posted at 2020-12-21

はじめに

※この記事は、SAP Advent Calendar 2020 の22日目の記事として投稿しています。

気が付けば年の暮れですね。
今年はほぼ1年間コロナウイルスとの戦いでした。
特に飲食業界では客足が減ったり細心の注意を払って接客したりと、かなり大変なようです。
そこで今回はお客さんとの接触を出来るだけ避けての営業を可能とする、わんこそば自動お代わり機をご紹介いたします。

概要

まずは動画をご覧ください。

機能概要.JPG

アプリを起動すると杯数カウント画面が立ち上がり、わんこそばをお代わりできる状態となります。
この状態でお代わりを頼む(=感圧センサーにお椀を乗せる)とロボットアームがお代わりを装い、また回転皿が動いてロボットアームにお代わりを補充します。
連動して画面には現在の杯数が表示されるようになっており、開始/おかわり/終了時に音声も出力されるようになっています。
アプリを終了するには水色のボタンを押す必要がありますが、このボタンはお代わりを頼んでいる状態(=感圧センサーにお椀を乗せている状態)でのみ反応します。ゆっくりしていると0.5秒後には次のお代わりが装われるため、すぐにボタンを押下する必要があります。
終了に成功したら金額が計算され、帳票が出力されます。
またその際SAP S/4HANAにアクセスして購買依頼伝票を作成します。

アーキテクチャ

Jetson Nano上ではわんこそばお代わり機能を構成するPythonアプリケーションと、帳票作成機能であるNode.jsアプリケーションの2種類を実装しています。
アーキ.JPG

必要なもの

製品 補足
Jetson Nano Raspberry Pi 4Bでも可
6軸ロボットアーム
360度回転サーボモータ
感圧センサー
プラスチック用コンパスカッター
プラスチック皿 x2
LEDボタン
お代わり機の台座の各種部品 木材とかを組み合わせて作ってください
SAP S/4 HANA 1909    オンプレミスver

パッケージ構成

.
├── display_window
│     ├── templates
│     │      └── index.html
│     ├── source
│     │      └── 各種画像の素材
│     └── flaskapp.py
├── create_check
│     ├── node_modules
│     ├── index.js
│     └── package.json
├── sound
│     └── 各種音声ファイル
├── button.py
├── imgedit.py
├── main.py
├── okawari.py
├── pressure_sensor.py
├── purchase_create.py
└── sound.py

実装手順

1. 感圧センサーの設定
2. お代わり機の作成
3. 杯数カウントの画面作成
4. 音声出力機能の追加
5. 終了ボタンの作成
6. 帳票作成
7. SAP S/4 HANA上での購買依頼伝票の作成

1. 感圧センサーの設定

お代わりのトリガーには感圧センサーを用います。感圧センサーからはアナログ信号が出力されるため、A/Dコンバーターを用いてデジタル信号に変換するようにしてください。配線の方法とソースコードは以下の記事をそっくりそのまま使わせてもらいました。
Raspberry Piで感圧センサー(ALPHA-MF02-N-221-A01 )の情報取得するところまで

pressure_sensor.py
import wiringpi as pi
import time

class MCP3002:
    def __init__( self, ss, speed, vref ):
        self.ss = ss
        self.speed = speed
        self.vref = vref

        pi.wiringPiSPISetup( self.ss, self.speed )

    def get_value( self, ch ):

        # mcp3002 データシート Table 5-1やFigure 6-1を参考
        # ch0 0x6800(0b01101000), ch1 0x7800(0b01111000)
        senddata = 0x6800 |  ( 0x1800 * ch ) 

        buffer = senddata.to_bytes( 2, byteorder='big' )

        pi.wiringPiSPIDataRW( self.ss , buffer )

        # buffer[0]に上位2bitが入っているのでシフトする
        # buffer[1]に下位の数字が入っているので足す
        # 0x3ff(0b1111111111)なのでANDをとって1023以上は切り捨てられる
        value = (( buffer[0] << 8 ) + buffer[1] ) & 0x3ff

        return value

    def get_volt( self, value ):
        return value * self.vref / float( 1023 )

2. お代わり機の作成

お代わりが補充される仕組みですが、6箇所の穴を開けた皿(皿①)と1箇所だけ穴を開けた皿(皿②)の2枚を上下に重ね合わせ、上方の皿をサーボモータで回転させることによりお代わりが下に落ちるようにします。

皿①(表面)

皿①(裏面)

皿②(表面)

皿②(裏面)

皿①+皿②

皿及びホルダーとなるプラスチック製の容器の穴は、プラスチック用コンパスカッターで力技でくり抜きます。(皿が硬いと生半可なコンパスカッターでは切れません。今回は1000円程度のコンパスカッターを使用しましたが、1穴開けるのに30分弱かかっていました。)
またサーボモータは360度以上回転するタイプのものと180度までのものの2種類がありますが、今回は必ず360度以上回転するものを選ぶようにしましょう。

次に、実際にお代わりを装うロボットアームについて説明します。ロボットアームは合計6つのサーボモーターからなり、直接Jetson Nanoから全てのサーボモーターを制御することは出来ないので、間にサーボモータードライバーを噛ませる必要があります。

先程の回転皿のサーボモータも一緒にサーボモータードライバーに繋げてやりましょう。またロボットアームとサーボモータードライバーは外部電源に接続して電力を供給しています。

またお代わりを装うためのお椀を用意し穴を空け、ロボットアームのエンドエフェクター(ハンドの部分)に装着します。

上記の回転皿とロボットアームを連動させることでお代わりを実現します。今回は各オブジェクトの位置関係を固定値で入れているため、少しでも配置がズレると想定通りに動いてくれません。そこで、適当に台座を工作してロボットアームと回転皿の位置を固定しましょう。

上の台座にロボットアームと回転皿を取り付けます。

ロボットアームと回転皿を動かすためのロジックは以下の通りです。

okawari.py
import sys
import time
# Import the PCA9685 module.
sys.path.append('/home/user/Adafruit_Python_PCA9685/')
import Adafruit_PCA9685

class Arm:
    def __init__(self):
        #初期設定
        self.pwm = Adafruit_PCA9685.PCA9685()
        # Set frequency to 60hz, good for servos.
        self.pwm.set_pwm_freq(60)
        self.pwm.set_pwm(8, 0, 202)
        time.sleep(0.5)
        self.pwm.set_pwm(3, 0, 200)
        time.sleep(0.5)
        self.pwm.set_pwm(1, 0, 400)
        time.sleep(0.5)
        self.pwm.set_pwm(2, 0, 200)
        time.sleep(0.5)
        self.pwm.set_pwm(4, 0, 400)
        time.sleep(0.5)
        self.pwm.set_pwm(5, 0, 400)
        time.sleep(0.5)
        self.pwm.set_pwm(0, 0, 600)
        time.sleep(0.5)

    def okawari(self, counter):
        #おわんをひっくり返す
        self.pwm.set_pwm(0, 0, 150)
        time.sleep(1)
        self.pwm.set_pwm(0, 0, 600)
        time.sleep(1)

        #回転皿の下ロボットアームを移動(一気に目標地点まで動かすと勢い余って柱に衝突するため、目標に近づくと少しずつ動かす)
        self.pwm.set_pwm(5, 0, 250)
        time.sleep(0.2)
        self.pwm.set_pwm(5, 0, 240)
        time.sleep(0.2)    
        self.pwm.set_pwm(5, 0, 230)
        time.sleep(0.2)    
        self.pwm.set_pwm(5, 0, 220)
        time.sleep(0.2)    
        self.pwm.set_pwm(5, 0, 210)
        time.sleep(0.2) 

        #回転皿からそばを落とす
        self.pwm.set_pwm(8, 0, 202 + (40 * (counter-1)))
        time.sleep(2)

        #元の位置にアームを戻す        
        self.pwm.set_pwm(5, 0, 400)
        time.sleep(2)    

3. 杯数カウントの画面作成

画面作成にかけられる時間が少なかったため、「サーバを立ち上げてURLにアクセスすると、杯数を表示する」という至ってシンプルな作りの画面を採用しました。サーバの立ち上げにはflaskを使用しています。

まずは画面に表示するための画像を作成します。以下のような画像をフレームとして用意しておきます。
wankosoba_frame.png

また0~9の数字の画像も同様に準備しておきます。
display.JPG

上記フレームと数字の画像を組み合わせて、その時の杯数に応じた画面を作成するためのコードを実装します。

imageedit.py
import time
from PIL import Image, ImageDraw, ImageFilter, ImageTk
import os.path

class ImageEditor:
    def __init__(self):
        #画像ファイルを読み出し
        self.image_wanko = Image.open('/home/user/ドキュメント/自動わんこそば/display_window/source/image_wanko/wankosoba_frame.png')
        self.images = [''] * 10
        for i in range(10):
            self.images[i] = Image.open('/home/user/ドキュメント/自動わんこそば/display_window/source/image_wanko/%d.png' % i)

    def edit(self, counter):
        backup_image = ''
        backup_image = self.image_wanko.copy()

        if counter <= 9:
            backup_image.paste(self.images[counter], (270, 50))

        elif 10 <= counter <= 99:
            counter_10th = str(counter)[0]
            counter_1th = str(counter)[1]
            backup_image.paste(self.images[int(counter_10th)], (210, 50))
            backup_image.paste(self.images[int(counter_1th)], (270, 50))
        elif 99 <= counter <= 999:
            counter_100th = str(counter)[0]
            counter_10th = str(counter)[1]
            counter_1th = str(counter)[2]
            backup_image.paste(self.images[int(counter_100th)], (170, 50))
            backup_image.paste(self.images[int(counter_10th)], (225, 50))
            backup_image.paste(self.images[int(counter_1th)], (280, 50))
        else:
            print("食べ過ぎです")

        backup_image.save('/home/user/ドキュメント/自動わんこそば/display_window/static/image_tmp.png', quality=95)

実際に出来上がる画像は以下のようになります。
image_tmp.png

画面に表示する画像の準備が整ったので、次にflaskを用いてサーバを立ち上げるためのコードを書きます。わざわざネットワーク通信する必要もないので、localhost指定で良いと思います。

falskapp.py
# server.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.after_request
def add_header(r):
    """
    Add headers to both force latest IE rendering engine or Chrome Frame,
    and also to cache the rendered page for 10 minutes.
    """
    r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    r.headers["Pragma"] = "no-cache"
    r.headers["Expires"] = "0"
    r.headers['Cache-Control'] = 'public, max-age=0'
    return r

def execute():
    app.run(host='localhost', port=8080)

if __name__ == '__main__':
    app.debug = True
    app.run(host='localhost', port=8080)

URLにアクセスした場合に呼び出されるindex.htmlファイルを実装します。↑で作成した杯数画像をimgタグで指定しています。「?php ...?」の部分についてですが、これが無いと画像内の杯数が更新されても、読み込むファイル名は同じであるためキャッシュから古い画像を取ってきてしまい、画面上での画像が更新されない状態となります。これを防ぐために「?php ...?」でファイルにタイムスタンプを押し、読み込み画像が最新となるようにしています。

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>WankosobaApp</title>
</head>
<body>
  <br>
  <img id="image_place" src="/static/image_tmp.png?<?php echo date('YmdHis'); ?>" />
</body>
</html>

4. 音声出力機能の追加

今回はmp3形式の音声ファイルを出力するので以下のように実装しました。wav形式の場合は少し書き方が異なるので注意してください。

sound.py
import pygame.mixer
import time
from mutagen.mp3 import MP3 as mp3

class Sound:

    def play(self, filename):
        # mixerモジュールの初期化
        pygame.mixer.init()

        # 音楽ファイルの読み込み
        pygame.mixer.music.load(filename)

        #音源の長さ取得
        mp3_length = mp3(filename).info.length

        # 再生
        pygame.mixer.music.play(1)

        #再生開始後、音源の長さだけ待つ(0.25待つのは誤差解消)
        time.sleep(mp3_length + 0.25)

        # 再生の終了
        pygame.mixer.music.stop()

5. 終了ボタンの作成

見覚えのあるボタンを終了ボタンとして使用します。配線は「たらい落としでSAP S/4 HANA上に購買依頼伝票を作成してみた」を参考にしてください。

ロジックは以下の通りです。

button.py
import time
import subprocess
import RPi.GPIO as GPIO

class Button:
    def __init__(self):
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(24, GPIO.IN) 

    def isPressed(self):
        print(GPIO.input(24))
        #LEDボタンが押下されている場合
        if GPIO.input(24) == GPIO.HIGH:    
            return True
        else:
            return False

6. 帳票作成

お会計の帳票作成はSCPの帳票作成サービスであるSAP Cloud Platform Forms by Adobeを使用します。私の師匠である鬼瓦先生の記事「SAP Forms by Adobe使ってみた」を参考に実装しました。index.jsを呼び出せばPDF形式で以下のような帳票が出力されるようになっています。
帳票.JPG

7. SAP S/4 HANA上での購買依頼伝票の作成

そばが品切れになると困るので、いつも通りS/4HANA上で購買依頼伝票を作成します。
APIは前回の記事「コロナで危険度の高い人を見つけたらすぐ治療しようとするアプリ作ってみた」の「3. SAP Cloud SDK for JavaScriptを用いたAPI実装」で作成したものをそのまま呼び出しています。

purchase_create.py

import urllib.request
import urllib.error 
import lxml
import base64
import json
import re
import ssl
from bs4 import BeautifulSoup
from collections import defaultdict

class HttpClient :

    #POSTメソッドによる購買依頼伝票の作成
    def createPurchaseReq():

        url = <<APIのエンドポイント>>
        #ヘッダ情報を設定
        headers = {"Content-Type" : "application/json"}

        #ボディ情報を設定
        json_obj = {
            "matnr": "300002",
            "menge": 1,
            "meins": "PC"
        }

        # POSTリクエスト送信        
        json_body = json.dumps(json_obj).encode("utf-8")

        req = urllib.request.Request(url, data=json_body, headers=headers, method='POST')
        try:
            with urllib.request.urlopen(req) as response:

                #レスポンスを取得する        
                response_body = response.read().decode("utf-8")
                print()
                # JSONとして読み込む
                json_obj  = json.loads(response_body)

                # 値の取り出し
                pr_number = json_obj['matnr']
                print('購買依頼伝票:' + pr_number + 'を作成しました。')

        #例外処理        
        except urllib.error.HTTPError as err:
            soup = BeautifulSoup(err, "lxml")
            print(soup)

最後に実行用のファイルを用意します。

main.py
from __future__ import division
from pressure_sensor import MCP3002
from purchase_create import HttpClient 
from okawari import Arm
from imgedit import ImageEditor
from button import Button
from sound import Sound
import time
from selenium import webdriver
import subprocess
#import sys
#sys.path.append('/home/user/ドキュメント/自動わんこそば/create_check/')

#感圧センサーをインスタンス化
adc = MCP3002(0, 1000000, 3.3)
#ロボットアームをインスタンス化
arm = Arm()
#ボタンをインスタンス化
button = Button()

okawari_flag = False
counter = 1

#音声を出力する「いらっしゃいませ」
Sound().play('./sound/door.mp3')
Sound().play('./sound/welcome.mp3')

#ブラウザに表示する画面の初期化
img_editor = ImageEditor()
img_editor.edit(counter)
#ブラウザ上で表示
driver = webdriver.Chrome()
driver.get("http://localhost:8080/")

while True:
    #感圧センサーの値を取得
    value = adc.get_value(0)
    print(value)
    time.sleep(0.5)

    #そばがない状態でおわんがセンサー上に置かれた場合
    if (500 <= value):
        if okawari_flag == False:
            okawari_flag = True
            counter += 1

            #終了ボタンが押されていない場合はおかわりを装う
            if button.isPressed() == False:

                #画面更新処理
                img_editor.edit(counter)
                driver.refresh()

                #音声を出力する「はい、どうぞ」
                Sound().play('./sound/haidouzo.mp3')

                #アームを動かしておかわり
                arm.okawari(counter)

            #終了ボタンが押されている場合は終了処理を行う
            else:
                time.sleep(1)

                #音声を出力する「ありがとうございました」
                Sound().play('./sound/thanks.mp3')
                Sound().play('./sound/thanksossan.mp3')

                #お会計の伝票を出力する
                print('帳票を出力します')
                with open('/home/user/ドキュメント/自動わんこそば/create_check/number.txt', mode='w') as f:
                    f.write(str(counter))
                subprocess.check_call('sudo node /home/user/ドキュメント/自動わんこそば/create_check/index.js', shell=True)
                print('帳票を出力しました')
                time.sleep(1)

                #購買依頼伝票を作成
                print('購買依頼伝票を作成します')
                HttpClient.createPurchaseReq()

                break
    else:
        okawari_flag = False

完成

これで冒頭のような動きのわんこそば自動お代わり機が完成します。Flaskサーバを立てた上でmain.pyを実行しましょう。
7227557C-BED8-4150-BFD2-9716A070B943.jpeg

残課題

動画では素晴らしい出来栄えのわんこそば自動お代わり機ですが、残念ながら現時点で以下のような課題が残っており、現場で使用するには改修が必要です。

①ロボットアームの動きがガクガクしている
今回の実装だとアームの速度が0か1の二択となってしまいガクガクしてしまいます。アームの速度がシグモイド曲線を描く(=動き始めと終わりはゆっくりで、途中は早く動く)ように出来ればもっとカッコ良く動くはずで、ROSを使えば実現できそうなので今後検証を進めていきたいです。

②お代わり用のそばが回転皿の隙間に詰まるバグがある
回転皿を構成する2枚の皿の間には少し隙間が空いており、隙間にそばが詰まって停止することがあります。100均のお皿の底を削って無理やり使用していますが、3Dプリンタを使用して隙間が無くなるような作りにすれば解決すると思います。

③500回に1回くらいの割合で回転皿が暴走する
稀に回転皿が暴走して回り続けることがあり、原因不明の状態です。電気信号が変に残っていたりするせいなのかもしれませんが、再現性が無いため半ば諦めています。

BGM/音声のリンク

動画で使用させていただいたフリーのBGMや音声の取得元を以下に記載いたします。

H/MIX GALLERY
http://www.hmix.net/music_gallery/image/asian.htm

あみたろの声素材工房
https://www14.big.or.jp/~amiami/happy/voice_01.html

効果音ラボ
https://soundeffect-lab.info/sound/voice/line-girl1.html

Wingless Seraph
https://wingless-seraph.net/material-voice.html

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
What you can do with signing up
4