大学祭でシステムを作った話（レシートプリンタ・受付システム編）

どんな背景でシステムを開発・運用したかは、[昨日のアドベントカレンダー](https://qiita.com/KentaroAbe/items/df6365fdeacf2210746e)をご覧ください

## 何を作ったのか
来場者の属性（性別・年代・職業など）をあらかじめ取得しておき、訪れた企画の履歴を紐づけることで、どのような属性の人がどのような企画に行く傾向があるのかや、来場人数のカウント、混雑の度合いなどを分析するするためのシステムです。

そして、企画を訪れた際にタイムスタンプを付与するための媒体として、QRコードを発行し、読み取るためのシステムがあります。

## ハードウェア構成
昨年度の受付システムでも、同様のシステムが稼働していました。
その際に購入したレシートプリンタがあったので、今年もそれを使用しました。
[メーカー公式サイト](https://www.star-m.jp/products/s_print/mcprint/sp01_13.html)

タブレットと接続できるようなタイプのレシートプリンタは、最近軽減税率やキャッシュレスなどで、Airレジのようなタブレットを使ったPOSレジで見たことあるような方もいらっしゃるのではないでしょうか。

大体、1台につき5万円くらいするみたいです[（参考リンク）](https://store-store.jp/products/detail.php?product_id=195)
それが、大学祭実行委員会には3台あったので、15万円・・・ ~~どんだけ予算余ってたんだ~~
このレシートプリンタは、法人だけでなく、個人でも使えるSDKが提供されているのが特徴です。

レシートプリンタは購入でしたが、タブレットは昨年度レンタルで、既になかったので、iPadを調達しました。
Androidで用意するなり、ラズパイとかで開発すればもっと安く済んだのかもわかりませんが、如何せん時間がなかったので、開発が慣れているiPadを採用することにしました。

レシートプリンタとiPadとの接続方法はいくつかあります。
![IMG_0689.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/48736442-f807-8f17-28ce-7eef555f4536.png)


- Lightningケーブルで有線接続
- Bluetoothで無線接続
- LAN接続

Bluetoothが一番接続はスッキリしていますが、今回調達したiPadはアプリのデプロイをローテーションしながら行ったり、企画での受付に拠出するために合計6台あり共通運用なのでペアリングの動作が大変でした。
LANも大学の制約で事前の申請が必要且つ自分でネットワークを構築するとなかなか大変なので、今回Lightningケーブルで有線接続としました。
ペアリングする手間なく、APIでコネクションを確立すればそのまま印刷が可能な上、iPadに給充電することができます。
使うコンセントがiPadとプリンタのペアで1本で済むので、ケーブルが気にならないなら最も簡単で確実じゃないかと思います。
![IMG_0688.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/f0c77242-511a-cbd2-91f7-c6d81fc0d39e.png)
これが有線の場合の最小構成です。
レシートプリンタには、電源ケーブルが繋がっています。

## SDKの使い方（Swift）
SDKの導入方法は、公式のドキュメントを参照してください（SDKのファイルと一緒にPDFで同梱されています）
Objective-Cで書かれているため、Swiftで使うにはブリッジングヘッダなるファイルを作成しなければならないようです。

レシートプリンタとの接続は、AppDelegateでインスタンスを作成して行います。
ViewControllerで保持する形式にしたかったのですが、~~やっぱり時間がなかったので~~

```swift:AppDelegate.swift
import UIKit
import Alamofire
import KeychainAccess
import SwiftyJSON

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, StarIoExtManagerDelegate {
    var manager:StarIoExtManager!
    
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        manager = StarIoExtManager.init(type: .standard, portName: "BT:mC-Print3", portSettings: "", ioTimeoutMillis: 10000)!
        manager.delegate = self
        manager.connectAsync()
        
・・・以下略・・・
```

`manager`が今回のミソで、各ViewControllerなどから呼び出すことになります。
`StarIoExtManager`を初期化する際に与える引数のうち、`portName`は、Bluetoothを使う場合でも有線を使う場合でも同じものを使用するようです。
`AppDelegate`には、`StarIoExtManagerDelegate`を継承させておきます。
（今回はあまり出番がありませんでしたが、プリンタのカバーが開いたり、ロール紙が切れた際の処理を記述することができます。）

実際の印刷のコードがこちらです。

```swift:ReceptionViewController.swift
func printQrCode(user_id:String){
        let ap = UIApplication.shared.delegate as! AppDelegate 
        //AppDelegateで保持しているプリンタとのコネクションのインスタンスを取得する
        
        do{
            let builder = StarIoExt.createCommandBuilder(.starPRNT)! //プリンタへ送信する命令の構築用

            //第一引数に与えたdataの文字列からQRコードを生成して命令へ追加する
            builder.appendQrCodeData("https://app.iniadfes.com/visitor?user_id=\(user_id)".data(using: .utf8)!, model: .no2, level: .L, cell: 10)

            //行ごとに、印刷する文字列をデータ化して命令へ追加する
            builder.appendLineFeed() //空白で若干下げる
            builder.appendData(withLineFeed: "QRコードを受付で提示してください".data(using: .shiftJIS))
            
            let formatter = DateFormatter()
            formatter.dateFormat = "MM月dd日"
            builder.appendData(withLineFeed: "来場日：\(formatter.string(from: Date()))".data(using: .shiftJIS))
            builder.appendData(withLineFeed: "ーーーーーーーーーーーーーーーー".data(using: .shiftJIS))
            builder.appendData(withLineFeed: "お帰りの際、受付でのアンケートにご協力をお願いいたします".data(using: .shiftJIS))
            builder.appendData(withLineFeed: "ーーーーーーーーーーーーーーーー".data(using: .shiftJIS))

            //用紙カット
            builder.appendCutPaper(.fullCutWithFeed)
            
            var command = [UInt8]()
            let command_data = NSData.init(bytes: builder.commands.mutableBytes, length: builder.commands.length)
            command = [UInt8](Data(command_data))
            
            var total: UInt32 = 0
            while total < UInt32(command.count) {
                var written: UInt32 = 0
                // 印刷データを送信し続ける
                try ap.manager.port.write(writeBuffer: command, offset: total, size: UInt32(command.count) - total, numberOfBytesWritten: &written)
                total += written
            }
        }catch{
            let alert = UIAlertController(title: "Error", message: "QRコードの印字に失敗しました", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            alert.addAction(UIAlertAction(title: "リトライ", style: .destructive, handler: {action in
                self.printQrCode(user_id: user_id)
            }))
            
            self.present(alert, animated: true, completion: nil)
            return
        }
        
    }
```

命令構築用の`StarIoExt.commandBuilder`の初期化の際の引数には、対象のデバイスの種類を指定します。
今回は`.starPRNT`を指定しましたが、キャッシュドロワーのような他の種類のデバイスも存在しているようでした。
1行ずつデータを追加していくというのが、何となく直感的でした。

文字コードは`Shift-JIS`だったり`UTF-8`だったり、どっちにするべきなのかイマイチよく分からなかったです・・・
最初`Shift-JIS`で印字していたところ、なぜか文字化けするようになって`UTF-8`に変更して解決→再び文字化けするようになって戻したということがあったので、その辺ちゃんと仕様見返します・・・
~~そしてなんでまだ`NSData`とか生きてるんだよ~~

![IMG_0691.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/74e06779-b26c-1a42-7695-75ff48d2ff3c.png)

こんな感じで出力されました。

## アプリ周り
全体受付用のiPadアプリです。
また、昨年と同じ紙のQRコードの他、公式アプリでもQRコードを表示できるようにしました。（その関係は明日のアドベントカレンダーで書きます）

<img width="768" alt="IMG_0003.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/e420e964-2445-b8b3-8247-3af62a49580a.png">

アプリのQRであっても、来場したフラグを持っていないと実際の来場者数をカウントできない（アプリ側はリセット操作でQRコードを何度でも発行できてしまうので）ので、そのための画面です。

<img width="768" alt="IMG_0004.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/b45fda2b-9497-c819-e566-3d3e2575abf2.png">

紙のQRが必要な場合は、ここで属性情報を入力します。

<img width="375" alt="IMG_FDFA53B3508C-1.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/97ea9adb-d6f6-28ce-b16a-03c73f64b91c.jpeg">

その他、企画の受付では、公式アプリに内蔵している機能を使って受付します。（本来はQRを表示するところを、権限のあるアカウントでログインすることで読み取り機能をアクティベートしています）
QRを検出すると、自動でAPIに受付のリクエストを送信し、すぐに次のQRを読み取れるようになっています。

## 権限管理
今回、このシステムを使うにあたってのユーザー管理は、Googleアカウントを使いました。
もちろん、Gmailとかのフリーアドレスでなく、大学で使われているG Suiteアカウントです。（他組織のG Suiteなどでもログインはできないように検証処理をしています）
（本当なら大学のOpenAMを使いたかったけども）予めどのIDでログインを許可するかや、どのサークルの読み取りができるかのポリシーを用意しています。（そこだけサークルの代表者に一覧を提出させました）

なお、実行委員は実行委員用のroleを割り当てて全て読み取り可にしています。

その辺りの管理も、企画管理システムと一体になっていて、Webから編集が可能です。
<img width="1680" alt="スクリーンショット 2019-12-14 1.52.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/d8bc0529-8301-9af7-73cd-209c4c1e473b.png">
<img width="1680" alt="スクリーンショット 2019-12-14 1.53.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92661/bad78495-3d19-e50f-f9ba-bd1e579d9a48.png">

## 来場者レポート
最終的に集まったデータは、グラフ化して表示できます（実物のデータになっちゃうのでスクショはごめんなさい）
受付の権限の範囲で、サークル参加者も一部閲覧できるようにして、共有しました。

## 運用してみて
QRコードも、サードパーティーのもので読み取ってログイン済みのブラウザで開き・・・というルーチンをやめて、ネイティブで連写できるようにしたことで、オペレーションもかなり改善したと思います。
また、後からGUIで諸々の設定を変更可能にしているので、トラブルになったとしても臨機応変に対応できたのかなと思います。