この記事は SAP Advent Calendar 2019 の12月26日分の記事として執筆しています。

はじめに
初めまして。サイコパスのりおです。
初投稿です。よろしくお願いします。
早速ですが、皆さんはこんなことを思ったことはないでしょうか?「ボタン1つで伝票が出来たらいいのに」と。
また、こんなことも思ったことはないでしょうか?「たらい落としを味えたらいいのに」と。
そんな欲張りなあなたの願いを叶えるため、本記事では画期的なたらいシステムを実装していきます。
たらいシステムの概要
まずタライを落とし、頭上のLEDボタンを押下します。
ボタンがONの状態になると、回路を伝ってRaspberry PiのGPIOピンに電流が流れ、ピンの電位が高くなります。
この電位を検知してPython Scriptが実行され、SAP S/4 HANA上で作成したODataサービスにPOSTを行い、購買依頼伝票を作成します。
必要なもの
| 製品 | 補足 | 
|---|---|
| Raspberry Pi 3 MODEL B+ | OSはRaspbianを使用 | 
| SAP S/4 HANA 1709 | オンプレミスver | 
| LEDプッシュボタン | 100均で購入 | 
| 銅線 | |
| 抵抗(330Ω, 3.3kΩ) | |
| ブレッドボード | |
| ジャンパーワイヤー(オス-メス) | |
| ジャンパーワイヤー(オス-オス) | 
実装手順
1.Raspberry PiとLEDボタンの接続
2.S/4 HANA上でのODataサービスの実装
3.Raspberry PiからODataサービスへのPOSTの実装
1. Raspberry PiとLEDボタンの接続
100均(ダイソー)で購入したこちらのLEDボタンを改造し、Raspberry Piに接続します。

まずLEDボタンを開封し、裏面のネジを4つ外して分解します。

LEDライトとスイッチはそのまま利用しますが、電力供給元はRaspberry Piとしたいので、電池ボックスへ繋がる銅線は+/−共に切ってしまいます。
銅線が短い場合は、別途購入した銅線を適度につなぎ合わせてください。

ボタンカバーに穴を開けて銅線を外に出し、最初に外した4本のネジを取り付けると、LEDボタンの準備は完了です。

次にこれをRaspberry Piに接続します。
回路は以下のように組みました。抵抗は330Ω, 3.3kΩのものを使用しています。
 
実際の回路はこのようになっています。
 
使用するLEDボタンの種類によっては抵抗やRaspberry Pi側の出力電圧も変えないとうまくいかないと思うので、各自調節してください。(自分の場合は3.3V電源に接続しても上手くいきませんでした)
また、LEDボタンから出る銅線をミノムシリード線で接続していますが、ジャンパーワイヤーでも問題ありません。
最後に、LEDボタンが押された場合にメッセージが出力されるようなPython ScriptをRaspberry Pi上で書きましょう。
import time
import subprocess
import RPi.GPIO as GPIO
# ボタン押下検知
pinnumber=24
GPIO.setmode(GPIO.BCM)
GPIO.setup(pinnumber, GPIO.IN) 
sw_flag = False  #LEDボタンの状態。初期状態はOFF
while True:
    #LEDボタンが押下されていない場合
    if GPIO.input(pinnumber) == GPIO.LOW:
        #LEDボタンの状態をOFFにする
        sw_flag = False
    #LEDボタンが押下されている場合
    else:
        #LEDボタンがOFFからONに切り替わった場合に後続処理を行う。
        if sw_flag == False:
            #LEDボタンの状態をONにする
            sw_flag = True
            print("zukahira")
while Trueで常に入力側のGPIOピン(GPIO:24)にかかる電圧を監視し、LEDボタンが押されて回路が繋がるとピンの電圧が高くなり、後続処理へと進むようになっています。(今はシェルにメッセージを出力するようにしていますが、こちらはステップ3の後でODataサービスにPOSTを行うロジックに変更します。)
上手くいくとボタン押下時にこんな感じでメッセージが表示されます。

一つ注意が必要なのは、LEDボタンがONになった後自動的にOFFにならないことであり、単に「LEDがON ⇨ 伝票作成」というロジックを組んでしまうと、OFFにするまで無限に伝票が作成されてしまいます。
もちろん後続のPOSTによる通信には2,3秒時間を要するので、その間にOFFにすれば良い話ではありますが、危険であることには変わりありません。そのため、sw_flagという変数にLEDボタンのON/OFFの状態を記録しておき、OFFからONとなった時にのみ後続処理へと進むようにしています。
(もちろんLEDボタンは自然にOFFにはならないため、一度押したらもう一度押してOFFにしなければなりません。)
これでひとまずLEDボタンの準備は完了です。
2. S/4 HANA上でのODataサービスの実装
次にS/4 HANA上で、呼び出すODataサービスをSEGWを用いて実装します。
実装に際して以下の記事を参考にしてもろパクリ作成させていただきました。
リンク: 「だっふんだ 」と言うとSAP S/4HANAで購買依頼伝票を打てるiOSアプリを実装する(2/2)
GETとPOSTでアクセスすることになるので、上記リンクの記事を元にGetEntity, CreateEntityメソッドを作成(コピー)しましょう。
今回の実装では、サービス名は"YSASAKI_PR_CREATE_SRV"、エンティティセット名は"PurchaseReqSet"としました。
3.Raspberry PiからODataサービスへのPOSTの実装
urllibを使用して、ステップ2で作成したODataサービスにアクセスします。
POSTの場合にはXSRFトークンとcookieが必要となるので、まずGETでXSRFトークンとcookieを取得し、その情報をリクエストヘッダにのせてPOSTを行います。
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 HttpMethod:
    __url = None
    __user = None
    __password = None
    
    #コンストラクタの定義
    def __init__(self, url):
        self.__url = url
        self.__user = 'ユーザ名'
        self.__password = 'パスワード'          
    #GETメソッドによるCSRFトークン、cookieの取得
    def getMethod(self):
        #BasicAuthの設定
        baseuserpass = base64.b64encode('{}:{}'.format(self.__user, self.__password).encode('utf-8'))
        #ヘッダ情報をセット
        headers_getmethod ={'Authorization': 'Basic ' + baseuserpass.decode('utf-8'),
                            'X-CSRF-Token': 'Fetch'
                           }
        #リクエストを作成
        req_getmethod = urllib.request.Request(self.__url, headers=headers_getmethod)
        #SSL認証エラーを無視する(注:信頼できるサイトでのみ実行すること)
        ssl._create_default_https_context = ssl._create_unverified_context
        #リクエストを送信し、レスポンスを取得
        res_getmethod = urllib.request.urlopen(req_getmethod)
        #GETメソッドを通じてCSRFトークンを取得
        XCSRFToken = res_getmethod.info().get('x-csrf-token')
        #通信をcloseする
        res_getmethod.close();
        
        #次にレスポンスヘッダからcookieを取得する
        cookies = {}
        count = 1
        #3行目までがcookieに関する情報であり、それ以降の情報は取得不要
        for cookie in res_getmethod.info().values() :
            cookies['cookie' + str(count)] = cookie
            count += 1
            if count >= 4:
                break
        
        #CSRFトークンとcookieを返す
        return XCSRFToken, cookies
    
    #POSTメソッドによる購買依頼伝票の作成
    def postMethod(self, XCSRFToken, cookies):
        #BasicAuthの設定
        baseuserpass = base64.b64encode('{}:{}'.format(self.__user, self.__password).encode('utf-8'))
        
     
        #ヘッダに同一キーで複数の値をセットできるようにオブジェクトを作成
        headers_postmethod = defaultdict(list)
        #ヘッダ情報を設定
        headers_postmethod = {'Authorization': 'Basic ' + baseuserpass.decode('utf-8'),
                              'X-CSRF-Token' : XCSRFToken,
                              'Content-Type' : 'application/xml',
                              'cookie'       : cookies['cookie1'],
                              'cookie'       : cookies['cookie2'],
                              'cookie'       : cookies['cookie3']
                             }
        #ボディ情報を設定
        xmlPostBody = """<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<entry xml:base=\"http://ルートURL等/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/\" xmlns=\"http://www.w3.org/2005/Atom\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\">
         <id>http://ルートURL等/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet('000000000000300002')</id>
         <title type=\"text\">PurchaseReqSet('000000000000300002')</title>
         <updated>2019-11-28T07:00:49Z</updated>
         <category term=\"YSASAKI_PR_CREATE_SRV.PurchaseReq\" scheme=\"http://schemas.microsoft.com/ado/2007/08/dataservices/scheme\"/>
         <link href=\"PurchaseReqSet('000000000000300002')\" rel=\"edit" title="PurchaseReq\"/>
         <content type=\"application/xml\">
          <m:properties>
           <d:Matnr>000000000000200002</d:Matnr>
           <d:Menge>20</d:Menge>
           <d:Meins>PC</d:Meins>
          </m:properties>
         </content>
        </entry>""" 
        # POSTリクエスト送信
        bytesXMLPostBody = xmlPostBody.encode('UTF-8')
        req = urllib.request.Request(self.__url, data=bytesXMLPostBody, headers=headers_postmethod, method='POST')
        try:
            with urllib.request.urlopen(req) as response:
                
                #レスポンスを取得する        
                response_body = response.read().decode("utf-8")
                soup = BeautifulSoup(response_body, "lxml")
                print(soup)
                             
                #レスポンスから購買依頼伝票番号を取得する
                pr_number = re.sub('\\D', '', str(soup.title))
                return pr_number
                
        #例外処理        
        except urllib.error.HTTPError as err:
            soup = BeautifulSoup(err, "lxml")
            print(soup)
            
POST後はレスポンスボディをシェル上に表示し、また購買依頼伝票番号を呼び出し元に返すようにしておきます。
今回はOpenVPNを利用してS/4 HANAに直接アクセスしていますが、SAP Cloud Platformを経由する方が難易度は低いので、後者の手法をお勧めします。
完成?
ODataサービスへアクセスするためのクラスが完成したので、ステップ1で作成したpr_button.pyのメッセージ出力のロジックを書き換えましょう。
(追加するコードの左には+、削除するコードの左には-を記載しています。)
import time
import subprocess
import RPi.GPIO as GPIO
+import http_method
# ボタン押下検知
pinnumber=24
GPIO.setmode(GPIO.BCM)
GPIO.setup(pinnumber, GPIO.IN) 
sw_flag = False  #LEDボタンの状態。初期状態はOFF
while True:
    #LEDボタンが押下されていない場合
    if GPIO.input(pinnumber) == GPIO.LOW:
        #LEDボタンの状態をOFFにする
        sw_flag = False
    #LEDボタンが押下されている場合
    else:
        #LEDボタンがOFFからONに切り替わった場合に後続処理を行う。
        if sw_flag == False:
            #LEDボタンの状態をONにする
            sw_flag = True
-            print("zukahira")
+            #ODataサービスにアクセスするクラスをインスタンス化する
+            url = "https://ルートURL/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet"
+            http = http_method.HttpMethod(url)
+
+            #GETによりCSRFトークンとcookieを取得する
+            XCSRFToken, cookies = http.getMethod()
+
+            #POSTにより購買依頼伝票を作成する
+            pr_number = http.postMethod(XCSRFToken, cookies)
+
+            #伝票の作成に成功した場合
+            if pr_number != None:
+                print(pr_number)
そしてPR_Button.pyを実行してLEDボタンを押すと・・・

購買依頼伝票の作成に成功したようです!(都合上レスポンスボディは表示していません。)
念のためS/4 HANA上でも確認してみましょう。

ドリフ大爆笑DVDの購買依頼伝票が作成されました!おめでとうございます!!
、、、とここまで読んでいただいた方の中には、以下の点にお気づきになった方も多いのではないでしょうか。
・「スイッチをONにした後にいちいちOFFにするの、手間じゃない?」
・「1つずつしか商品を注文出来ないのは微妙」
・「わざわざスクリーンやキーボードをRaspberry Piに繋げるが面倒」
・「伝票が作られても無音なのが寂しい」
・「あれ、タライは? ・・・タライは??」
そこで、以下のパートでこれらの追加実装を行います。
追加で必要なもの
| 製品 | 補足 | 
|---|---|
| I2C接続LCD (AQM0802) | |
| ダクトスイッチ | |
| USBミニスピーカー | イヤホンでも可 | 
| たらい | ポリエステル製 | 
| なべのふた | いい感じで金属音が鳴るもの | 
| 帽子 | お好きなデザインのものを | 
| 粘着シート | 着脱自由なものを | 
追加の実装手順
4.LEDボタンの改良
5.ボタン押下時間に応じた発注数量の設定
6.I2C接続LCD(液晶表示ディスプレイ)の追加
7.音声ファイルの出力
8.タライの調整
4.LEDボタンの改良
今回使用するボタンもそうですが、基本的に100均で買えるLEDボタンは一度押すと、もう一度押されるまでONの状態を維持します。
現在の実装では再度POSTするためにいちいちOFFの状態まで戻す必要があり、無駄に手間がかかってしまいます。そこでLEDボタンを再度分解し、ボタンを押している間だけONとなるダクトスイッチに付け替えます。
(注:LEDボタンによってはサイズが大きすぎor小さすぎてダクトスイッチが上手く押されない場合があるので、実店舗で見てから買うことをお勧めします。)
そして、元々スイッチがあった場合にダクトスイッチをセットします。
用意したダクトスイッチは元々のスイッチよりも一回り大きかったため、瞬間接着剤で固定した後にビニールテープで補強しました。

そして上蓋を被せ、ネジで止めれば完成です。
5.ボタン押下時間に応じた発注数量の設定
ステップ4でLEDボタンの改造が完了したのでpr_button.pyの中のボタン押下ロジックを修正し、ボタンを押した時間に応じて数量が増加するようにしましょう。
ついでに、ボタンがずっと押された状態のままで大量の商品が注文されてしまう、という事故を防ぐため、5秒以上LEDボタンがONとなるとエラー終了となるようロジックを追加します。
import time
import subprocess
import RPi.GPIO as GPIO
import http_method
# ボタン押下検知
pinnumber=24
GPIO.setmode(GPIO.BCM)
GPIO.setup(pinnumber, GPIO.IN) 
-sw_flag = False  #LEDボタンの状態。初期状態はOFF
-while True:
-    #LEDボタンが押下されていない場合
-    if GPIO.input(pinnumber) == GPIO.LOW:
-        #LEDボタンの状態をOFFにする
-        sw_flag = False
-    #LEDボタンが押下されている場合
-    else:
-        #LEDボタンがOFFからONに切り替わった場合に後続処理を行う。
-        if sw_flag == False:
-            #LEDボタンの状態をONにする
-            sw_flag = True
+while True:
+    sw_counter = 0  #LEDボタンを押した時間分のカウントがセットされる
+    sw_start = 0  #LEDボタンを押し始めた時刻
+    sw_flag = False  #LEDボタンの状態。初期状態はOFF
+    error_flag = False  #5秒以上LEDボタンを押下した場合のフラグ
+
+    while True:
+        #LEDボタンが押下されている場合
+        if GPIO.input(pinnumber) == GPIO.HIGH:
+            #LEDボタンの状態をONにする
+            sw_flag = True
+            #LEDボタンを押し始めた時刻をセットする
+            if sw_start == 0:
+                sw_start = time.time()
+            
+            sw_middle = time.time()
+            #LEDボタンを押し始めてから5秒以上たった場合            
+            if (sw_middle - sw_start) >= 5:
+                #エラーをシェル上に表示。エラーフラグをONにして処理を終了する
+                print('ERROR')
+                error_flag = True                
+                break                                
+        
+        #LEDボタンが押下されていない場合
+        else:
+            #LEDボタンの状態がON→OFFとなった場合
+            if sw_flag == True:
+                #ボタンの押下終了時刻を取得し、開始時刻との差分をカウンタにセットする
+                sw_finish = time.time()
+                sw_counter = round((sw_finish - sw_start)*10)
+                #押下時間が0.05秒未満の場合は数量が0となるため、その場合は1をセットする
+                if sw_counter == 0:
+                    sw_counter = 1
+                
+                break
+    
+        #LEDボタンを押し始めてから5秒以上たっていた場合は処理終了
+        if error_flag == True:
+            break    
+    if error_flag == True:
+        continue    
    
    #ODataサービスにアクセスするクラスをインスタンス化する
    url = "https://ルートURL/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet"
    http = http_method.HttpMethod(url)
    
    #GETによりCSRFトークンとcookieを取得する
    XCSRFToken, cookies = http.getMethod()
    
    #POSTにより購買依頼伝票を作成する
-    pr_number = http.postMethod(XCSRFToken, cookies)
+    pr_number = http.postMethod(XCSRFToken, cookies, sw_counter)
    #伝票の作成に成功した場合
    if pr_number != None:
        print(pr_number)
上記のロジックではボタン押下時間0.1秒毎に数量を1加えていき、最終的な数量をHttpMethodインスタンスのpostMethodに渡すようにしています。
渡された数量をPOSTのBodyに組み込むよう、http_method.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 HttpMethod:
    __url = None
    __user = None
    __password = None
    
    #コンストラクタの定義
    def __init__(self, url):
        self.__url = url
        self.__user = 'ユーザ名'
        self.__password = 'パスワード'          
    #GETメソッドによるCSRFトークン、cookieの取得
    def getMethod(self):
        #BasicAuthの設定
        baseuserpass = base64.b64encode('{}:{}'.format(self.__user, self.__password).encode('utf-8'))
        #ヘッダ情報をセット
        headers_getmethod ={'Authorization': 'Basic ' + baseuserpass.decode('utf-8'),
                            'X-CSRF-Token': 'Fetch'
                           }
        #リクエストを作成
        req_getmethod = urllib.request.Request(self.__url, headers=headers_getmethod)
        #SSL認証エラーを無視する(注:信頼できるサイトでのみ実行すること)
        ssl._create_default_https_context = ssl._create_unverified_context
        #リクエストを送信し、レスポンスを取得
        res_getmethod = urllib.request.urlopen(req_getmethod)
        #GETメソッドを通じてCSRFトークンを取得
        XCSRFToken = res_getmethod.info().get('x-csrf-token')
        #通信をcloseする
        res_getmethod.close();
        
        #次にレスポンスヘッダからcookieを取得する
        cookies = {}
        count = 1
        #3行目までがcookieに関する情報であり、それ以降の情報は取得不要
        for cookie in res_getmethod.info().values() :
            cookies['cookie' + str(count)] = cookie
            count += 1
            if count >= 4:
                break
        
        #CSRFトークンとcookieを返す
        return XCSRFToken, cookies
    
    #POSTメソッドによる購買依頼伝票の作成
-    def postMethod(self, XCSRFToken, cookies):
+    def postMethod(self, XCSRFToken, cookies, sw_counter):    
        #BasicAuthの設定
        baseuserpass = base64.b64encode('{}:{}'.format(self.__user, self.__password).encode('utf-8'))
        
     
        #ヘッダに同一キーで複数の値をセットできるようにオブジェクトを作成
        headers_postmethod = defaultdict(list)
        #ヘッダ情報を設定
        headers_postmethod = {'Authorization': 'Basic ' + baseuserpass.decode('utf-8'),
                              'X-CSRF-Token' : XCSRFToken,
                              'Content-Type' : 'application/xml',
                              'cookie'       : cookies['cookie1'],
                              'cookie'       : cookies['cookie2'],
                              'cookie'       : cookies['cookie3']
                             }
        #ボディ情報を設定
        xmlPostBody = """<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<entry xml:base=\"http://ルートURL等/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/\" xmlns=\"http://www.w3.org/2005/Atom\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\">
         <id>http://ルートURL等/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet('000000000000300002')</id>
         <title type=\"text\">PurchaseReqSet('000000000000300002')</title>
         <updated>2019-11-28T07:00:49Z</updated>
         <category term=\"YSASAKI_PR_CREATE_SRV.PurchaseReq\" scheme=\"http://schemas.microsoft.com/ado/2007/08/dataservices/scheme\"/>
         <link href=\"PurchaseReqSet('000000000000300002')\" rel=\"edit" title="PurchaseReq\"/>
         <content type=\"application/xml\">
          <m:properties>
           <d:Matnr>000000000000200002</d:Matnr>
-           <d:Menge>20</d:Menge>
+           <d:Menge>%d</d:Menge>
           <d:Meins>PC</d:Meins>
          </m:properties>
         </content>
-        </entry>"""
+        </entry>"""% sw_counter #ボタンの押下秒数x10の値を数量にセット
        # POSTリクエスト送信
        bytesXMLPostBody = xmlPostBody.encode('UTF-8')
        req = urllib.request.Request(self.__url, data=bytesXMLPostBody, headers=headers_postmethod, method='POST')
        try:
            with urllib.request.urlopen(req) as response:
                
                #レスポンスを取得する        
                response_body = response.read().decode("utf-8")
                soup = BeautifulSoup(response_body, "lxml")
                print(soup)
                             
                #レスポンスから購買依頼伝票番号を取得する
                pr_number = re.sub('\\D', '', str(soup.title))
                return pr_number
                
        #例外処理        
        except urllib.error.HTTPError as err:
            soup = BeautifulSoup(err, "lxml")
            print(soup)
ちなみに、たらいシステムならではの障害ですが、たらいがボタンを押してから離れるまでの時間は驚くほど早く、基本的に押下時間が0.05秒未満となって発注数量が0となってしまうので、数量が0の場合は1をセットするようpr_button.pyにロジックを組んでいます。
6.I2C接続LCD(液晶表示ディスプレイ)の追加
Raspberry Piを使ったことのある方ならご共感いただけるかもしれませんが、Raspberry Piは使用する際にスクリーン、キーボード、マウス、電源に接続しなければまともに操作することが出来ず、購買依頼伝票を作るためだけにこれらの機器を繋げ、Python Scriptを実行するのは非常に手間のかかる作業です。
そのため画面はHDMI接続のスクリーンではなく軽量なI2C接続のLCD(AQM0802)を繋げたままにし、またPythonスクリプトがRaspberry Pi起動時に自動的に実行されるよう設定することで、マウスやキーボードの接続の手間を省きましょう。
まずI2C接続LCDについてですが、こちらは文字通りI2C(クロックに同期させてデータの通信を行う同期式シリアル通信)接続を行う液晶表示ディスプレイです。
今回使用するAQM0802はその1種で、Raspberry Piに接続後はコマンド送信(初期設定等)→データ送信(表示したい文字列の送信)によって画面を制御します。
 ↑AQM0802
↑AQM0802
コマンドプロンプト上でも操作は可能ですが、Python上でも手軽な操作を可能とするsmbusというライブラリがあるので、こちらを使用してLCDの画面を制御します。
まず、AQM0802をRaspberry Piの1,3,5,7,9,11番目のピンにそのまま差し込みましょう。

次にRaspberry Piの設定でI2C接続を有効化します。

そしてコマンドプロンプトで"i2cdetect -y 1"を実行しましょう。以下のように表示されたら無事AQM0802がRaspberry Piに認識されています。

接続を確認後、AQM0802を制御するクラスを実装します。
import smbus
import time
import RPi.GPIO as GPIO
# LCD AQM0802
class LCD:
    __i2c = None
    
    def __init__(self):
        #コネクションオブジェクトを取得
        self.__i2c = smbus.SMBus(1)
        
    #コマンド送信
    def command(self, code):
            self.__i2c.write_byte_data(0x3e, 0x00, code)
            time.sleep(0.1)
    
    #データ送信  
    def writeLCD(self, message):
            mojilist=[]
            for moji in message:
                    mojilist.append(ord(moji)) 
            self.__i2c.write_i2c_block_data(0x3e, 0x40, mojilist)
            time.sleep(0.1)
    #LCDの初期設定        
    def init(self):
            self.command(0x38)  #画面サイズ指定
            self.command(0x39)  #拡張コマンド設定
            self.command(0x14)  #Internal OSC Frequency
            self.command(0x70)  #コントラスト
            self.command(0x56)  #3.3V指定
            self.command(0x6c)  #Followerコントロール
            time.sleep(0.5)  #200ミリ秒以上空ける
            self.command(0x38)  # 拡張コマンド設定完了
            self.command(0x01)  #画面クリア
            self.command(0x0f)  #画面表示ON
initメソッドで行うコマンド送信は、初期化に必要な手順の集まりなんだなという認識で十分です。
writeLCDメソッドは引数に表示したい文字を渡すと、AQM0802にその文字を表示します。
それではlcd.pyで定義したLCDクラスをpr_button.py内でインスタンス化し、LCDを制御しましょう。
この際、作成された伝票の番号をAQM0802に表示するよう、ロジックを修正します。
import time
import subprocess
import RPi.GPIO as GPIO
import http_method
+import lcd
+#LCDのインスタンスを作成し、初期化処理を行う
+lcd = lcd.LCD()
+lcd.init()
+lcd.writeLCD('HELLO')
+time.sleep(0.5)
+#ネットワーク接続の確認
+while True:
+    ping = subprocess.Popen(["ping", "-w", "3", "-c", "1", '172.217.26.36'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+    ping.communicate()
+
+    #ネットワークに接続されている場合はLCDにその旨を表示し、後続処理へ
+    if ping.returncode == 0:
+        lcd.command(0x01)  #画面クリア
+        lcd.writeLCD('kanamen')
+        break
+    #ネットワークに接続されていない場合はLCDに"PLEASE WAIT"と表示し、3秒毎に接続を確認する
+    else:
+        lcd.command(0x01)  #画面クリア
+        lcd.writeLCD('PLEASE')
+        lcd.command(0x40+0x80)  #LCDの2行目に遷移
+        lcd.writeLCD('WAIT')
+        time.sleep(3)
# ボタン押下検知
pinnumber=24
GPIO.setmode(GPIO.BCM)
GPIO.setup(pinnumber, GPIO.IN) 
while True:
    sw_counter = 0  #LEDボタンを押した時間分のカウントがセットされる
    sw_start = 0  #LEDボタンを押し始めた時刻
    sw_flag = False  #LEDボタンの状態。初期状態はOFF
    error_flag = False  #5秒以上LEDボタンを押下した場合のフラグ
    while True:
        #LEDボタンが押下されている場合
        if GPIO.input(pinnumber) == GPIO.HIGH:
            #LEDボタンの状態をONにする
            sw_flag = True
            #LEDボタンを押し始めた時刻をセットする
            if sw_start == 0:
                sw_start = time.time()
            
            sw_middle = time.time()
            #LEDボタンを押し始めてから5秒以上たった場合            
            if (sw_middle - sw_start) >= 5:
-                #エラーをシェル上に表示。エラーフラグをONにして処理を終了する
-                print('ERROR')
+                #エラーを画面に表示。エラーフラグをONにして処理を終了する
+                lcd.command(0x01)  #画面クリア
+                lcd.writeLCD('ERROR')
+                time.sleep(5)                
                error_flag = True                
                break                                
        
        #LEDボタンが押下されていない場合
        else:
            #LEDボタンの状態がON→OFFとなった場合
            if sw_flag == True:
                #ボタンの押下終了時刻を取得し、開始時刻との差分をカウンタにセットする
                sw_finish = time.time()
                sw_counter = round((sw_finish - sw_start)*10)
                #押下時間が0.05秒未満の場合は数量が0となるため、その場合は1をセットする
                if sw_counter == 0:
                    sw_counter = 1
                break
    
        #LEDボタンを押し始めてから5秒以上たっていた場合は処理終了
        if error_flag == True:
            break    
    if error_flag == True:
        continue    
    
    #ODataサービスにアクセスするクラスをインスタンス化する
    url = "https://ルートURL/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet"
    http = http_method.HttpMethod(url)
    
    #GETによりCSRFトークンとcookieを取得する
    XCSRFToken, cookies = http.getMethod()
    
    #POSTにより購買依頼伝票を作成する
    pr_number = http.postMethod(XCSRFToken, cookies, sw_counter)
    #伝票の作成に成功した場合
    if pr_number != None:
-        print(pr_number)
+        #作成された購買依頼伝票番号をLCDに出力する
+        lcd.command(0x01)  #画面クリア
+        lcd.writeLCD('PR:' + pr_number[0:6])
+        lcd.command(0x40+0x80)  #LCDの2行目に遷移
+        lcd.writeLCD(pr_number[5:10])
そして、Raspberry Pi起動時にpr_button.pyが実行されるよう、systemd下のserviceファイルにその旨を記載します。
 
再起動し、何もしなくてもAQM0802に文字が表示されれば成功です。
 
これで電源にさえ繋げば、購買依頼伝票が作成される環境が整いました。
7.音声ファイルの出力
最後にpygameというライブラリを用いて、好きな音声ファイルを再生しましょう。
import pygame.mixer
class Sound:
    
    def sound_wav(self, file_name):
        # mixerモジュールの初期化
        pygame.mixer.init()
        # 音楽ファイルの読み込み
        sound = pygame.mixer.Sound(file_name)
        # 再生
        pygame.mixer.Sound.play(sound)
        # 再生の終了
        pygame.mixer.music.stop()
sound_wavメソッドにwav形式の格納先を渡すと、その音声ファイルが再生されます。mp3等も使用できますが、その場合はロジックが若干異なりますのでご注意ください。
それでは伝票作成の成功時にsound_wavメソッドを呼び出すようpr_button.pyファイルを修正しましょう。
import time
import subprocess
import RPi.GPIO as GPIO
import http_method
import lcd
+import sound
# LCDのインスタンスを作成し、初期化処理を行う
lcd = lcd.LCD()
lcd.init()
lcd.writeLCD('HELLO')
time.sleep(0.5)
+#音声に関するクラスをインスタンス化しておく
+music = sound.Sound()
# ネットワーク接続の確認
while True:
    ping = subprocess.Popen(["ping", "-w", "3", "-c", "1", '172.217.26.36'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    ping.communicate()
    #ネットワークに接続されている場合はLCDにその旨を表示し、後続処理へ
    if ping.returncode == 0:
        lcd.command(0x01)  #画面クリア
-        lcd.writeLCD('kanamen')
+        lcd.writeLCD('READY')
        break
    #ネットワークに接続されていない場合はLCDに"PLEASE WAIT"と表示し、3秒毎に接続を確認する
    else:
        lcd.command(0x01)  #画面クリア
        lcd.writeLCD('PLEASE')
        lcd.command(0x40+0x80)  #LCDの2行目に遷移
        lcd.writeLCD('WAIT')
        time.sleep(3)
# ボタン押下検知
pinnumber=24
GPIO.setmode(GPIO.BCM)
GPIO.setup(pinnumber, GPIO.IN) 
while True:
    sw_counter = 0  #LEDボタンを押した時間分のカウントがセットされる
    sw_start = 0  #LEDボタンを押し始めた時刻
    sw_flag = False  #LEDボタンの状態。初期状態はOFF
    error_flag = False  #5秒以上LEDボタンを押下した場合のフラグ
    while True:
        #LEDボタンが押下されている場合
        if GPIO.input(pinnumber) == GPIO.HIGH:
            #LEDボタンの状態をONにする
            sw_flag = True
            #LEDボタンを押し始めた時刻をセットする
            if sw_start == 0:
                sw_start = time.time()
            
            sw_middle = time.time()
            #LEDボタンを押し始めてから5秒以上たった場合            
            if (sw_middle - sw_start) >= 5:
                #エラーを画面に表示。エラーフラグをONにして処理を終了する
                lcd.command(0x01)  #画面クリア
                lcd.writeLCD('ERROR')
+                #だっふんだの音声を流す
+                music.sound_wav ('/home/pi/Documents/PurchaseRequestButton/だっふんだ.wav')
                time.sleep(5)                
                error_flag = True                
                break                                
        
        #LEDボタンが押下されていない場合
        else:
            #LEDボタンの状態がON→OFFとなった場合
            if sw_flag == True:
                #ボタンの押下終了時刻を取得し、開始時刻との差分をカウンタにセットする
                sw_finish = time.time()
                sw_counter = round((sw_finish - sw_start)*10)
                #押下時間が0.05秒未満の場合は数量が0となるため、その場合は1をセットする
                if sw_counter == 0:
                    sw_counter = 1
                break
    
        #LEDボタンを押し始めてから5秒以上たっていた場合は処理終了
        if error_flag == True:
            break    
    if error_flag == True:
        continue    
    
    #ODataサービスにアクセスするクラスをインスタンス化する
    url = "https://ルートURL/sap/opu/odata/sap/YSASAKI_PR_CREATE_SRV/PurchaseReqSet"
    http = http_method.HttpMethod(url)
    
    #GETによりCSRFトークンとcookieを取得する
    XCSRFToken, cookies = http.getMethod()
    
    #POSTにより購買依頼伝票を作成する
    pr_number = http.postMethod(XCSRFToken, cookies, sw_counter)
    #伝票の作成に成功した場合
    if pr_number != None:
        #作成された購買依頼伝票番号をLCDに出力する
        lcd.command(0x01)  #画面クリア
        lcd.writeLCD('PR:' + pr_number[0:6])
        lcd.command(0x40+0x80)  #LCDの2行目に遷移
        lcd.writeLCD(pr_number[5:10])
+        #ちょっとだけよ〜の音声を流す
+        music.sound_wav ('/home/pi/Documents/PurchaseRequestButton/ちょっとだけよ〜.wav')
これで購買依頼伝票の作成に成功したときに音声ファイルが再生されるようになります。
USBスピーカーでもイヤホンでも音声を流すことが出来るので、お持ちのものを接続しましょう。
8.たらいの調整
お待たせしました。ついにたらいの登場です。といっても本物の金だらいは洒落にならないくらい危険です。金だらい(2.1kg)を頭上1mから落下させると、LEDボタンに達する頃には小型ハンマー(300g)を時速45kmで振り下ろすのに匹敵するくらいのエネルギーを持ってしまい、LEDボタンはおろか頭部まで破壊する可能性が生じます。(金だらいの危険性は、あの志村けんご本人ですら言及しているくらいです。)
そのため、今回はポリプロピレンで出来たたらいを使用します。
 
ただ、このままだとたらいで皆さんが期待するような良い金属音が鳴らないので、たらいになべのふたをさくっと実装します。
使用するなべのふたは100均で購入した、何の変哲もないふたです。こちらをまずは分解しましょう。

次に、カッターナイフを用いてなべのネジが通るサイズの穴を開けます。

たらいの内側と外側からなべのふたとつまみで挟むようにし、ネジで固定しましょう。
するとたらいの底の一部がなべのふたでコーティングされたようになります。
ふたの中心部はたらいと密着しているため大して良い音は鳴りませんが、ふちに近づくにつれて良い音が出ます。
そのため、少々テクニックがいりますが、たらいは多少中心からずれるようにして落としましょう。

ちなみにたらいの内側はこんな感じでつまみがひょっこり出ています。

また、フリーハンドでLEDボタンを頭の上に乗せられるように、着脱可能な粘着テープで帽子とLEDボタンを接着しましょう。
使用する粘着テープは以下のものを使用しました。

まずLEDボタンの底面に粘着テープを貼り、はみ出した部分をカットします。

次に帽子の上に粘着テープを貼りましょう。こちらの帽子はお好みのもので構いません。

粘着テープにより、LEDボタンと帽子がいい感じでくっ付いています。
これを被れば仕事の最中だろうが食事中だろうがたらいを受けることが出来ます。

最後に、念のためはかりを用いてLEDボタンの押下に必要な力を測定しましょう。
はかりにボタンを押しあてて少しずつ力を加えていき、ボタンが押された瞬間の重さを3回測定して平均を取ったところ、389gでした。

一方で、今回用意したたらいの重さは377gだったので、数ミリでもたらいが上にあればLEDボタンがONとなるようです。

完成!
以上のステップを経て、ようやく完成です。
追加実装で少し煩雑になってしまったので、完成版たらいシステムをフロー図で整理しました。

では実際にたらいを落として購買依頼伝票が作成される様子をご覧ください。
たらい落としでSAP S/4 HANA上に購買依頼伝票を作成してみた#IoT #SAP #RaspberryPi #Python #たらい pic.twitter.com/vveJXTY4Re
— 鬼瓦鋼弊 (@kohei_onigawara) December 25, 2019
このたらいシステムを活かして皆さんもガンガン伝票を作成していきましょう。
それでは良いお年を!
参考URL
・「だっふんだ 」と言うとSAP S/4HANAで購買依頼伝票を打てるiOSアプリを実装する(2/2)
・I2C接続AQMシリーズのキャラクタ表示LCDをラズパイで使う (1) AQM0802
・Netduinoシリアル通信(I2C)でLCD表示


