🔐

多店舗展開するジムの会員入退室管理を材料費数万円で実現し、24時間営業にした話

2024/07/13に公開
5

ジムの会員管理システムを作った僕に「エニタイムフィットネスみたいなことがしたい」とジムを家族経営するお客さんから相談された。

「えっ!?会員管理を作ったついでにエニタイムフィットネスみたいな仕組みをやりたい!?予算は無い!?不正防止のため、入退室時の写真も撮りたい?!ログもとりたい!?」

さすが筋トレに明け暮れてるオーナーさんの要望はマッチョだと思った。

普通にやれば電子錠の仕組みや工事やらで一店舗あたり数百万から一千万掛かるような仕組みだろう。

そんな予算無いみたいだし、既存の店舗をそんな大々的に工事もできない。そもそも自分にそんな工事の知識もない。

結果Raspberrypiを使い、それを一店舗予算10万円代で実現、会員カードを他店舗と共有した24時間営業にできた。

その詳しい技術的な内訳を共有する。

(なお執筆時点では2024年だが、これ自体は5年前、2019年の仕事である。)

前提

  • 当時県内で3店舗ほど家族経営されていたフィットネスジム
  • その時点では朝10時くらいから夜10時までスタッフが常駐される時間だけ空けていた
  • それをさらに多店舗展開するにあたって24時間営業することになった
  • その時の会員管理システムを自分で作った
  • その時に鍵の入退室も含めて管理できないかと相談があった

システム内容

  • 鍵をICカードにかざした時に会員が正常なステータスかをAPIで判断
  • 正常なステータスであれば鍵を開錠
  • 共連れなどを防ぐため、その時の様子はカメラで撮影し、入退室ログとして残す

鍵の写真

## 鍵開錠の様子(開場後自動で閉じる)
https://youtube.com/shorts/cHkDXyWnqGQ?feature=share

https://youtube.com/shorts/L38OvMiYM4k?feature=share

検討した事

5年前は民泊などの影響もあり、ちょうど鍵のIOT機器やサービスが出始めたころであった。
そこで別のAkerunなどの開錠システムも検討したが、

  • こちらで制作したジムの会員管理とシームレスに連携できない

  • 電池式で不安
    (電池が切れた場合夜中に入れなくなる可能性がある。)

  • 要件として退出時にもカメラを撮影したいとリクエストされていた

  • 月額サブスクをクライアントが嫌がった

  • どうしてもネットワークを介して解錠命令を送るためカードをかざしたあとのラグが気になった

等の点により自前で実装することにした。

構成

結果、クライアントがネットで探してきた「NOAKEL」という端末を調べた所、これが使えそうだと思った。
NOAKELの画像

  • 電源式である
  • 接点入力が出来れば開錠命令をリモコンから発信できる

そこでRaspberryPiとICカードリーダーを使い、

またテストしたところ、ガラス戸レベルであれば外側から触れても接触にあまり問題なかったため、扉の内外両方からICカードをかざすことが可能であった。

機器の構成

図示するとこんな感じ

講師図

ソフトウェアの仕組み

図示するとこんな感じ

ソフトウェアの仕組み

RaspberryPiのプログラム

新規会員登録時にICカードのIDを紐づけてもらう作業を行い、それを会員に対して発行する。

会員が入室時にICカードリーダーをかざした際、読みとったIDを会員管理システムにAPIで確認し、OKな会員かNGな会員か(例えば滞納してるなど)をチェックする。

RaspberryPi側では、GPIOを操作し、タッチ音としてリレー出力で圧電ブザーを鳴らすと共に、cv2を使いRaspberryPiにUSBでつないだカメラでキャプチャを行い、それをRaspberryPiのシリアル番号と共にアプリケーション側に送る。

そして開錠OKな会員であれば、GPIOの接点出力モジュールからNOAKELに対して開錠命令を送る。

main.py
#!/usr/bin python
# -*- coding: utf-8 -*-
import os
import requests
import json
import binascii
import nfc
import threading
import time
import cv2
import base64
import mimetypes
import RPi.GPIO as GPIO
from classes import sound #My Definition Class
from classes import relay #My Definition Class
#from classes import camera #My Definition Class
from lib import functions #My Definition Functions



# Suica待ち受けの1サイクル秒
TIME_cycle = 1.0
# Suica待ち受けの反応インターバル秒
TIME_interval = 0.2
# タッチされてから次の待ち受けを開始するまで無効化する秒
TIME_wait = 3

GPIO_PORT = 4 #realy 

IMG_FILE = '/home/pi/amegym/image/picture.jpg'

API_URL = '[APIのURL]'
ACCESS_TOKEN = '[APIのアクセストークン]'

# NFC接続リクエストのための準備
target_req_felica = nfc.clf.RemoteTarget("212F")

# 106A(NFC type A)で設定
target_req_nfc = nfc.clf.RemoteTarget("106A")

cam = None

rasp_id = functions.getserial() #シリアルナンバーの取得

print ('Waiting for Tag...')
while True:
    # USBに接続されたNFCリーダに接続してインスタンス化
    clf = nfc.ContactlessFrontend('usb')
    # clf.sense( [リモートターゲット], [検索回数], [検索の間隔] )
    target_res = clf.sense(target_req_nfc,target_req_felica,  iterations=int(TIME_cycle//TIME_interval)+1 , interval=TIME_interval)

    # カメラデバイスを取得
    if cam is None:
        cam = cv2.VideoCapture(0)

        # カメラFPSを60FPSに設定
        cam.set(cv2.CAP_PROP_FPS, 30)
        cam.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
        cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)

    if not target_res is None:
        beep = sound.SOUND()
        beep.onRead()

        tag = nfc.tag.activate(clf, target_res)

        tag.sys = 3

        #IDmを取り出す
        idm = binascii.hexlify(tag._nfcid).upper()

        # readで画像をキャプチャ、imgにRGBのデータが入ってくる
        res, img = cam.read()

        # 保存
        cv2.imwrite(IMG_FILE, img )
        print('took picture!')
        prepend_info = 'data:%s;base64' % mimetypes.guess_type(IMG_FILE)[0]
        base_64_data = base64.b64encode(open(IMG_FILE, 'rb').read()).decode("ascii")
        image_data_base64 = '%s,%s' % (prepend_info, base_64_data)
        print('data encoded')

        request_args = {
            "RFID": idm,
            "DeviceID" : rasp_id,
            "access_token": ACCESS_TOKEN,
            "image" :   image_data_base64,
        }

        #session = requests.session()
        serverRes =  requests.post(API_URL, request_args)
        print(serverRes)
        if serverRes.status_code==200:
            print(serverRes.text.encode("utf-8"))
            res = serverRes.json()
            if res["msg"] == "success":
                print("relay on")
                GPIO.setmode(GPIO.BCM)
                GPIO.setup(GPIO_PORT, GPIO.OUT , initial = 1)
                GPIO.output(GPIO_PORT,1)
                time.sleep(0.5)
                GPIO.output(GPIO_PORT,0)
                GPIO.cleanup()

            else:
                beep.onFalse()
            #end if
        else:
            beep.onError()
            print(res)

        cam.release()
        cam = None


        del beep

        print('sleep ' + str(TIME_wait) + ' seconds')
        time.sleep(TIME_wait)


    clf.close()

#end while

まとめ

RaspberryPiも半導体の影響で値上がりしたが、材料費でNOAKELを入れても数万円でから十数万円程度で店舗を24時間営業にすることが出来た。

これを現在6店舗で展開しているが、大きなトラブルも無く5年間運用している。

Discussion

romaroma

日本語の理解が不足してましたらすみません
材料費でNOAKELを入れても数万円でから十数万円程度と書かれてますが
一店舗予算10万円では赤字だったのでしょうか?

Tomohide Hirakawa(平川知秀)Tomohide Hirakawa(平川知秀)

わかりにくくてすみません。クライアントも見る可能性あるので、詳しい予算や原価はぼかしてます。一店舗あたり、ノアケル入れて20万以内で収まったと思います。当時の原価など、詳しく知りたければxなどからお問い合わせくださればお伝えします。

romaroma

ご回答ありがとうございます、繊細な事をうかがい失礼いたしました
非常に安価なシステムで驚きました