ちょっと話題の記事

[レジなし無人販売冷蔵庫] 遂に完成しました\(^o^)/

2021.01.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

CX事業本部の平内(SIN)です。

Amazon Web Services ブログでは、「レジなし無人販売冷蔵庫を構築についての記事」が公開されています。
レジなし無人販売冷蔵庫を構築できる、This is my Smart Cooler プログラムを公開しました

こちらでは、「お客様自らがレジなし無人販売冷蔵庫を迅速に構築し学習や体験ができる This is my Smart Cooler プログラムを発表します。」ということで、そのレシピがGithubで公開されています。
レジ無し無人販売冷蔵庫 構築レシピ

「これを真似てみたい」ということで、逐次作業を進めてきたのですが・・・遂に完成しました。\(^o^)/

と、言いながら、完成度は、超低いのですが、一応「一通り」動作できるようになったので、一旦「完了」とさせて頂きました。

ちなみに「一通り」とは、以下の動作をさしています。

利用者動作

  • AmazonPayのQRコードをかざしてドアロックを解除する
  • 商品を取り出して扉を閉じると、取り出した商品を検出する
  • 商品の決済をAmazonPayで行う

メンテナンス動作

  • メンテナンス用のQRコードをかざしてドアロックを解除する
  • 商品を追加して扉を閉じると、冷蔵庫内の在庫が更新される

最初に、動作しているようすをご確認下さい。 動画の前段は、冷蔵庫の操作を外部から撮影したもので、後半は、その時のJetson Nanoの画面となっています。

2 構成

構成は、以下のようになっています。

エッジ側で主に動作しているのは、Jetson Nanoです。各種デバイスの制御に加えて、冷蔵庫内の商品の推論も行っています。

Amazon Payへの決済も、エッジ側からRestAPIで行うことが可能なのですが、APIキー(秘密鍵)が必要なので、操作できる範囲を制約するという意味でコードをLambda側に置いて、これをInvokeする形式としました。

その他、商品のマスターは、DynamoDBに、取り出した商品の発話にPolloyを使用し、AWSリソースへのアクセス権付与をCognitoのPoolIDで行っています。

3 動作フロー

動作のフローは、下記のようなイメージです。

QRコードが読み込まれた場合、そのコードが、メンテナンス用(今回、123456としています)かどうかで、大きく2つの流れに分かれます。

4 AWS側

AWS側のリソース(DynamoDb,Lambda,Cognito)構築は、CDKで行っています。

CDKで作成されるリソースは、概ね以下のとおりです。

(1) Lambda


https://github.com/furuya02/smart_cooler/blob/master/cdk/smart_cooler/lib/lambda/index.ts

(2) DynamoDB

(3) Role

CognitoのPoolIDに付与されるロールは、以下のようになります。

<br />{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "dynamodb:Scan",
            "Resource": "arn:aws:dynamodb:ap-northeast-1:xxxxxxxxxxxx:table/SmartCoolerProducts",
            "Effect": "Allow"
        },
        {
            "Action": "polly:SynthesizeSpeech",
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:smart_cooler_amazon_pay",
            "Effect": "Allow"
        }
    ]
}

詳しく、下記をご参照下さい。
https://github.com/furuya02/smart_cooler/blob/master/cdk/smart_cooler/lib/smart_cooler-stack.ts

5 エッジ(Jetson Nano)側

Jetson Nanoで動作するコードのメイン処理は、以下のようになっています。

※コードは、一部省略されています。詳しく、下記をご参照下さい。
https://github.com/furuya02/smart_cooler/blob/master/jetson/index.py

# 動作モード
class MODE(Enum):
    IDLE = 0 # 待機中
    WAIT = 1 # ドアが閉じられるのを待つ
    INFERNCE = 2 # 推論
    PROCESSING = 3 # 処理
    DELAY = 4 # 

class Main():
    def __init__(self):
        # 動作モード
        self.__mode = MODE.IDLE
        # メンテナンス(商品追加)モード
        self.__isMentenance = False
        # Session取得
        session = createSession(poolId, region)
        # 在庫管理
        self.__inventoryManager = InventoryManager(session, region, tableName)
        # AmazonPay
        self.__amazonPay = AmazonPay(session, region)
        # 商品検出モデル
        self.__model = Model(self.__inventoryManager.short_names, height, width)
        # カメラ映像
        self.__video = Video(deviceId, width, height)
        # ドアのロック
        self.__doorLock = DoorLock()
        # スピーカー
        self.__speaker = Speaker(session, region)
        # ドアの開閉検出
        DoorSensor(self.__on_change_door_status)
        # QRコードリーダー
        QrcodeReader(self.__on_read_qr_code)

        self.__run()

    # 動作モードの変更
    def __setMode(self, mode):
        self.__mode = mode

    # 推論処理
    def __inference(self):
        (result, self.__frame) = self.__model.inference(self.__frame)
        return result

    # AmazonPayでのQRコード確認 chargePermissionId取得
    def __check_qr_code(self, scanData):
        responce = self.__amazonPay.scan(scanData)
        if(responce["statusCode"] == 200):
            return responce["body"]["chargePermissionId"]
        return None

    # AmazonPayでの購入処理
    def __purchase(self, chargePermissionId, amount):
        response = self.__amazonPay.charge(chargePermissionId, amount, merchantNote, merchantStoerName, merchantOrderId)
        if(response["statusCode"] == 200):
            return True
        return False

    # ドアの開閉の変化
    def __on_change_door_status(self, status):
        str = "ドアが、開けられました" if status == DOOR_STATUS.OPEN else "ドアが、閉じられました"
        print(str)

        # ドアが閉じられた時
        if(status == DOOR_STATUS.CLOSED):
            # ドアをロックする
            print("ドアロック")
            self.__doorLock.enable() 
            if(self.__mode == MODE.WAIT):
                # 推論処理へ
                self.__setMode(MODE.INFERNCE)

    # QRコードの読み込み
    def __on_read_qr_code(self, qr_code):
        print("QRコードが読み込まれました: {}".format(qr_code))

        # メンテナンス用のコードかどうかの判定
        self.__isMentenance = False
        if (qr_code == "123456"):
            print("メンテナンスモード")
            self.__isMentenance = True
        else:
            # 有効なQRコードかどうかの判定
            self.__chargePermissionId = self.__check_qr_code(qr_code)
            if(self.__chargePermissionId == None):
                print("QRコードが無効")
                return
        print("ドアロック解除")
        self.__doorLock.disable() 
        self.__setMode(MODE.WAIT)

        if(self.__isMentenance):
            self.__speaker.fixed(PHRASE.REFIL)
        else:
            self.__speaker.fixed(PHRASE.PICKUP)

    # メインループ
    def __run(self):
        while(True):
            # MODE.DELAY中だけ、画像を更新しない
            if(self.__mode != MODE.DELAY):
                self.__frame = self.__video.read()
                if(self.__frame is None):
                    continue

            # 推論処理
            if(self.__mode == MODE.INFERNCE):
                result = self.__inference()
                self.__setMode(MODE.PROCESSING)

            # PROCESSINGモードで処理を行う
            if(self.__mode == MODE.PROCESSING):

                # 変更前の在庫
                before_stock = self.__inventoryManager.getStock()
                # 在庫数変更
                self.__inventoryManager.setStock(result)
                # 変更後の在庫
                after_stock = self.__inventoryManager.getStock()

                if(self.__isMentenance):
                    self.__speaker.fixed(PHRASE.UPDATE)
                else:
                    # 減少分のカウント
                    diff = []
                    for index in range(len(before_stock)):
                        diff.append(before_stock[index] - after_stock[index])

                    anount = 0
                    items = []
                    text = "お買い上げは、"
                    for index in range(len(diff)):
                        if(diff[index] != 0):
                            full_name = self.__inventoryManager.get_full_name(index)
                            price = self.__inventoryManager.get_price(index)
                            items.append(" >> {} {}円 {}個".format(full_name, price, diff[index]))
                            text += "{} {}円 {}個、".format(full_name, price, diff[index])
                            anount += price * diff[index]
                    if(anount != 0):

                        print("\n >> 取り出された商品は、以下のとおりです\n")
                        for item in items:
                            print(item)
                        print("\n >> 合計{}円\n".format(anount))

                        text += "合計で、{}円です。".format(anount)
                        self.__speaker.free(text)

                        # 購入処理
                        if self.__purchase(self.__chargePermissionId, anount):
                            print("AmazonPayの購入処理を完了")
                            self.__speaker.fixed(PHRASE.THANKS)
                        else:
                            print("AmazonPayの購入処理に失敗")

                # MODE.DELAYで10秒間待機
                self.__setMode(MODE.DELAY)
                self.__delay_counter = 10


            if(self.__mode == MODE.DELAY):
                print("DELAY {}".format(self.__delay_counter))
                self.__delay_counter -= 1
                time.sleep(1)
                if(self.__delay_counter <= 0):
                    print("待機中")
                    self.__setMode(MODE.IDLE)

            # 画像表示 終了キー'q'が押された場合Falseが返される
            if(self.__video.show(self.__frame) == False):
                break

Main()

また、エッジ(Jetson Nano)側で動作する全てのコードは、以下に置きました。
https://github.com/furuya02/smart_cooler/blob/master/jetson/

  • index.py
  • doorLock.py (ドアをロックするクラス(制御ピン GPIO23))
  • inventoryManager.py (在庫管理クラス)
  • speaker.py (スピーカーからメッセージを再生するクラス)
  • amazonPay.py (AmazonPayによる決済クラス)
  • doorSensor.py (センサーをGPIO18とGNDに接続して、ドアの開閉を検出するクラス)
  • model.py (DLRによる物体検出モデルの推論クラス)
  • video.py (OpenCVでWebカメラの画像を取得・表示するクラス
  • cognito.py (CognitoのPoolIdでSessionを取得する)
  • qrcodeReader.py (QRコードリーダの入力を処理するクラス)

6 最後に

非常に時間がかかってしまいましたが、一応動くものができて本当に嬉しいです。

なんと言っても、Amazon Web Services ブログ記事を真似させて頂いただけなのですが、機械学習からセンサー操作やAmazonPayの決済など、これでもかと言うほど勉強になりました。

改めて、下記の記事に感謝申し上げます。
レジなし無人販売冷蔵庫を構築できる、This is my Smart Cooler プログラムを公開しました
レジ無し無人販売冷蔵庫 構築レシピ

なお、AWSの記事では、エッジ側が、GreengrassのLambdaで構築されおり、CI/CDも紹介されています。 私の力量で敵わない部分は、色々自己流になってしまっている事をお許し下さい。

最後に、ここまで進めてきた作業のリンクを、ここに置かせて下さい。