4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

N高グループ・N中等部Advent Calendar 2024

Day 2

キャンフェス(キャンパス文化祭)でPOSシステムを作った話。

Last updated at Posted at 2024-12-01

導入

おはようございます こっちゃんです

まず最初に自己紹介をします

  • N高等学校
  • 通学コース 大宮キャンパス
  • 高校3年
  • 進路から必死に逃走中。

12/2になりましたが書き終わりませんでした(泣)
なので追記しまくります。

今回は、キャンパスフェスティバル(文化祭)でPOSシステムを構築した話です。

このシステムは共同で制作したものです。
主な共同制作者:(https://github.com/chikinaN)

事のながれ

イノキャン
(N/S高等学校大宮キャンパスの中で活動しているプログラミンググループの名称)

はキャンフェス(キャンパス文化祭)で紅茶を出すことになりました。
しかし自分たちはプログラミング勢の集まりなので、ただ飲料を提供するだけでは面白くないと考えたため、POSシステム、モバイルオーダーを作ろうと考えた。

全体の構成

camfes.drawio.png
矢印のついた線はSocket.ioの通信を示してます。
CF TunnnelはCloudflare Tunnelのことです。
が!今回は触れません。

開発に使ったもの

ハードウェア

  • レシートプリンタ
  • iPad(2台)
    • POS(レジ)用
    • 厨房用
  • ラズパイ(2台)
    • APIサーバー用
    • HTTPサーバー用
  • モニター+スティックPC(2台)
    • 呼び出し画面表示用
    • 呼び出し画面表示用

使用した技術、言語

  • Node.js
  • Socket.io
  • SQL
  • HTML+Javascipt+CSS
  • React
  • Python

APIサーバーの役割は以下のようになっています

  • 注文を受け取る
  • DB操作をする
  • レシートを印刷する

システム、コード解説、動作風景

システムやコードについての説明です。

API

使用モジュール等

  • express: HTTPサーバーを構築するためのフレームワーク。
  • http: Node.jsのHTTPサーバーモジュール。
  • socket.io: WebSocket通信を提供するモジュール。
  • sync-mysql: 同期的にMySQLにアクセスするためのライブラリ。非同期だと動かない箇所があったため使用
  • exec: 子プロセスを作成して外部コマンドを実行するための関数。Pythonスクリプトを実行するためだけに使用

注文を受け取る

  let items = JSON.parse(message);

POSやモバイルなどからはJson形式で注文が飛んできます。

注文番号生成

  let orderID = "";

  let max = connection.query('SELECT MAX(INTERNAL_ID) FROM order_list')[0]["MAX(INTERNAL_ID)"];

  if (items[0]["type"] == "mobile") { orderID="M"; }
  orderID += (max+1).toString().padStart(3, '0');

モバイルとPOSの注文が混同しないように区別しています

データベースに注文を書き込み

  time = Math.round((new Date()).getTime() / 1000);
  connection.query('INSERT INTO order_list (INTERNAL_ID, timestamp, orderID, flag) VALUES (NULL, ?, ?, 0)', [time, orderID]);

  items.forEach((item, index) => {
    if(index === 0){ return; }
    connection.query('INSERT INTO order_item (INTERNAL_ID, orderID, item, quantity) VALUES (NULL, ?, ?, ?)', [orderID, item["item"], item["quantity"]]);
  });

ここでテーブルが2つに分かれている理由はなぜでしょうか。
答えは、汎用性を持たせるためです。

もし途中で「新商品を追加する。」なんて事になったとしても対処できます
image.png

レシート印刷

  let array = { "status": "success", "orderID": orderID };

  exec('/usr/bin/python3 ../camfes/print.py '+print_arg, function(err, stdout, stderr) {
    if (stdout) console.log('stdout', stdout);
    if (stderr) console.log('stderr', stderr);
    if (err !== null) console.log('err', err);
  });

これが肝心のレシート印刷です。
このあとprint.pyについて触れるのですが、ここだけまさかのハードコーディングです。
print_argの中は

M001 1 2 3 4

のようになっています。
(左から注文番号、個数、個数、個数、個数)

後処理

  socket.emit('order_end', array);

  new_data = [{ "orderID": orderID, "items": items.slice(1), "flag": 0 }];
  io.emit('order_share', new_data);
});

注文が通ったときにするべきことはまだあります。
注文表示用のモニターとキッチン端末の情報を更新しなければなりません。
そのため、APIに送られてきた注文を加工して、POSやモニターに送ります。

注文部(POS モバイル共通)

まずはAPIを叩き、商品名と商品価格を取得します

商品名と商品価格を取得するコード
    fetch(api_url+"/getprice")
      .then((response) => {
        return response.json()
      })

この実装にした理由ですが、急遽商品名や商品価格の変更があったとしても柔軟に対応するためです。ちなみに、stock(在庫)は使いませんでした。

POS

IMG_0003.PNG

注文送信
async function send_data(e) {
  e.preventDefault();
  if (document.querySelector("#total").textContent.charAt(0) == 0) {
    toastr.error('空注文はできません', 'エラー')
    return false;
  }

  let data = [];
  data.push({type: "pos"})
  Array.from(document.getElementById("menus").children).forEach((a) => {
    let amount = a.querySelectorAll(".amount")[0];
    data.push({item: amount.dataset.name, quantity: amount.value})
  });

  blockUI.showOverlayAsync();
  socket.emit('order', JSON.stringify(data));
}

注文を送信するコード、この部分だけ見ても一見なんのことかわからないでしょう。
それもそのはず、短い期間での開発なのでゴリ押し実装が結構多いです。

 if (document.querySelector("#total").textContent.charAt(0) == 0) {

この部分は、合計値を表示する部分が「0円」の場合は注文を送信しないための処理です。
単価が0円の商品はないため。こんな処理で十分なのです。

 Array.from(document.getElementById("menus").children).forEach((a) => {

商品の個数が変更になる可能性があったため、商品名が存在する回数ループすることで
各商品ごとの注文個数を取得しています。

厨房

IMG_0004.PNG

モニター

image.png

print.py

レシートプリンタを操作するためにPython-escposというライブラリを使用しました。

p = Usb(0x416, 0x5011, in_ep=0x87, out_ep=0x06)

プリンタを指定するときにin_epとout_epを設定しないとうまく動きません。
最初に参考にしたサイトには書いていなかったのでつまずきました。

#注文部表示
draw.text((10,310), "--------------------------------", font=fontA, fill=0)
if (int(args[2]) != 0):
    draw.text((10,350), "ストレートティー(アイス)*{0: <3}  {1: >3,}ガリオン".format(int(args[2]),int(args[2])*STTEA_TANKA), font=fontAa, fill=0)

if (int(args[3]) != 0):
    draw.text((10,390), "ストレートティー(ホット)*{0: <3}  {1: >3,}ガリオン".format(int(args[3]),int(args[3])*STTEA_TANKA), font=fontAa, fill=0)

if (int(args[4]) != 0):
    draw.text((10,430), "ミルクティー(アイス)*{0: <5}  {1: >3,}ガリオン".format(int(args[4]),int(args[4])*MILKTEA_TANKA), font=fontAa, fill=0)

if (int(args[5]) != 0):
    draw.text((10,470), "ミルクティー(ホット)*{0: <5}  {1: >3,}ガリオン".format(int(args[5]),int(args[5])*MILKTEA_TANKA), font=fontAa, fill=0)

#合計
total_price = (int(args[2])*STTEA_TANKA)+(int(args[3])*STTEA_TANKA)+(int(args[4])*MILKTEA_TANKA)+(int(args[5])*MILKTEA_TANKA)
draw.text((10,500), "--------------------------------", font=fontA, fill=0)
draw.text((40,560), "小計:{: >6,}ガリオン".format(total_price), font=fontB, fill=0)

#注文コードの表示
if (args[1] != "mobile"):
    #draw.rectangle((10, 640, 390, 760), fill=0)
    font = ImageFont.truetype('mplus-1m-regular.ttf', 48, encoding='unic')
    draw.text((30,640), "こちらはモニターに表示されます、お客様の\nご注文番号です。今しばらくお待ちください。 ", 0 , font=fontS)
    draw.text((110,680), args[1], 0 , font=fontC)
else:
    draw.text((30,640), "ご利用ありがとうございました。", 0 , font=fontS)

注文部の表示です。ひどいコードですね。
さっきPOSのところで柔軟な対応とか言っておきながら、
印刷部分は決めうちのハードコーディングです。
キャンフェス当日までに商品名の変更が何度もあったので、何度も書き換えました()

モバイルオーダー

ここ担当したのは別の人です。自分より10000倍(10進数)くらいすごいプログラマーです。
ちきなという人間です。

設営

机に固定されるラズパイたち
A60E4F33-09F2-47C1-AED2-D1A188AC945E_1_102_o.jpeg

机の裏。(サーバールーム感あってかっこいい?)
4E3C3C4A-4410-40AA-AE14-E39A1C84A3D9_1_105_c.jpeg

実際の画面

POS用のiPad
image.png

キッチン用のiPad
フレーム-22-11-2024-11-16-07.tiff

呼び出し用モニター(もう一台は大きいモニターを使用)

反省と振り返り

技術的にはとても良い出来であった。
しかし実際の運用では商品を一杯作るのに時間がかかってしまいそこがボトルネックになってしまった。

また、当日は常に騒音に包まれており、イベントブースではモバイルオーダー注文者を特定するのが難しかった。
もし今後の人生で同じことをやる機会があれば、プッシュ通知などを実装しても良いかも。

先駆者様、参考にしたもの

文化祭で某チェーン店を再現して失敗した話
学祭で自作のレジと注文管理システムを開発・運用した話

4
0
1

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
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?