見出し画像

業務アプリ QRコードを使用した備品管理 No.021

Webixライブラリを使ったWEbアプリ(業務アプリ)を紹介していますが、今回は、より具体的な業務に活用できる機能としてスマホでQRコードを読取り、対象備品を保管する場所(棚や保管箱情報)と紐づける操作を最終的には紹介します。今回は、QRコードラベルの作成についてです。
QRコードは、テプラやPtouch製品で簡単に印刷できますが、業務アプリと連携して、印刷できる機能をまずは、作成します。業務アプリに備品情報を登録する操作の延長で、ラベルも連携して印刷できれば、備品番号や備品名の二重登録操作は不要で効率化と、ミスを防ぐ効果が期待できます。
今回紹介する機能は、テプラプリンタを使った連携機能です。テプラには、SPC10というパソコン向けの印刷アプリがありますが、このSPC10は、別のアプリ(EXCEL VBAも可能)から読み出して、自動印刷も可能です。事前に、印刷フォーマットの定義をSPC10で実施し、CSVファイルとの関連付け操作が必要ですが、その作業を実施後は、EXCEL VBAやC#アプリなどからCSVファイルを作成してSPC10を起動すれば、自動的にラベル印刷ができます。
業務アプリ(Webix+PHP)と連携することを検討した結果、以下の方式で実装することにしました。業務アプリ(Webix+PHP)はLinuxサーバで動作します。一方、ラベル印刷のソフトは、WindowsOSで動作するため、簡単には連携できません。SPC10には、印刷時に必要な情報を格納したCSVファイルとSPC10の起動が必要です。LinuxとWindows両方からアクセスできるフォルダを準備して、そのフォルダにPHPがCSVファイルを格納し、Windows側のアプリ(EXCEL VBAやC#アプリなど)で監視し、存在すれば、読み込み、SPC10を起動する動作も考えられますが、フォルダ環境の設定や常時監視の負荷などを考慮し、今回は、別の方式で実装してみます。WindowsアプリとしてC#アプリで作成します。(VisualStudio2022)
C#アプリに簡易Webサーバを構築して、PHPからは、curlコマンドでリクエストを送信し、C#アプリがその情報を受信し、CSVファイルを作成後に、SPC10を起動するイメージです。専用のサービスで簡易Webサーバを構築するので、URLとしては、http://パソコンのIPアドレス:8081/?code=A12302-11&label=テプラのように指定します。
備品番号や棚番号をcodeで指定し、備品名や棚名をlabelパラメータでリクエストすると、C#アプリは、そのリクエストを受信し、A12302-11,テプラ,"A12302-11,テプラ"の3つの列情報でCSVファイルを作成して指定フォルダに保存後に、SPC10.exeをC#から実行し、応答を相手に返します。
上記例のURLで自動印刷されるラベルは以下のようなものになります。
管理番号と備品名と、両方の情報を持ったQRコードです。

このラベルを該当備品や保管棚(棚の場合は、棚番号で作成)に貼り付けておくことで、スマホで、該当QRコードをスキャンすれば、以下のような操作が可能となります。
(1)備品QRコードスキャンで、スマホ画面に管理番号、備品名、保管棚番号を表示させ、格納先を確認できる操作
(2)棚QRコードと備品QRコードをスキャンして、該当備品の格納棚と関連づける操作
(3)Web画面から備品名を検索して、保管棚情報を検索したり、現状の状態を確認する。(保管中、持ち出し中、貸出中など)
前回、操作した日付情報なども表示することも可能です。
(4)棚情報を検索して、その棚に関連づいている備品の一覧を表示する操作
他にも、いろいろ考えることができます。
この記事では、上記ラベルをURLで指定して印刷する機能まで紹介します。
Windows10や11で常駐するアプリでかつ、フォーム(画面)を持ったアプリの実装には、マイクロソフトのVisualStudioC#が便利ですね。BasicよりC#をお勧めします。DBアクセス、Web連携などさまざまな機能実装が簡単に実装できます。ネットでも多くの記事が投稿されており、簡単です。今回は、2022のC#で簡単なアプリを実装してみました。
テプラの印刷連携は、該当URLからマニュアルやサンプル(EXCEL VBA)をダウンロードできます。

事前にラベルのフォーマットをSPC10で作成し、tpeファイルを作成しておきます。(画面フォーマットと連携CSVデータの関連づけをしたもの)
あとは、CSVファイルを作成して、いくつかのオプションパラメータを付与してSPC10.exeをC#から起動すれば、印刷できます。今回の例では、12mmのテープで印刷しています。6cmくらいの幅のテープになっていますが、デザインを変更すれば、もう少し幅を縮めることは可能です。
C#のアプリは、手動で情報を指定して印刷確認する機能も付与しました。

上記フォームの管理番号と品目名に情報を入力して手動実行をクリックすれば、TEPRAでQRコードが自動印刷できます。
実際の業務アプリでは、管理番号は、自動採番して、品目名や棚名など登録した情報からQRコードを印刷するので、上記の手動実行で印刷することは、事前の環境設定確認時だけかもしれません。
今回のアプリは、テプラSR750を使い、USB接続でPCに接続しています。パソコンリンク状態にして、PCの設定画面で、プリンタがオンラインになっていることを確認してから動作検証しています。
C#のアプリは、ログ出力用にNLogライブラリとURLパラメータをデコードするために、Microsoft.AspNet.WebApiをnugetでインストールしています。
http用のポート番号は、8081にしましたので、管理者権限で該当C#アプリが実行する必要があります。
以下にメインのソースと簡易Webサーバ用のclass HttpServerを紹介します。

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using NLog;
using System.IO;
using System.Configuration; //追加  同時に参照設定System.Configurationの追加も必要
using System.Diagnostics;   //追加 FileVersionInfoのため
using System.Reflection;    //追加  assemblyのため



namespace tepra_print
{
    public partial class Form1 : Form
    {
        //TEPRAに指定したデータを印刷する(管理番号+品名+QRコード)
        // 指定フォルダを監視
        //
        //   2024/03/25 初版 手動印刷機能  by sunsunfarm
        //
        public static Logger logger = LogManager.GetCurrentClassLogger();
        public static int INTERVAL_TIME;
        public static string TPE_FOLDER_INFO = @"D:\work\TEPRA\";  //TPEファイル格納フォルダ
        public static string TPE_FILE_INFO = "bihin_12.tpe"; // tpe_info 12mm幅テープ
        public static string CSV_FOLDER_INFO = @"D:\work\TEPRA\";  //CSVファイル格納フォルダ
        public static string CSV_FILE_INFO = "data.csv";

        public static string SPC10_CMD = @"C:\Program Files (x86)\KING JIM\TEPRA SPC10\SPC10.exe";  //SPC10インストールパス
        public static string COMMAND_PRM = "/p";
        public static string COMMAND_OPTION = "/C -f -h,/TW -off";  //テープカット(ハーフカット)

        public static string apps_version_info;

        public static long th_number;
        private Thread hMain;
        public static int HTTP_PORT;

        //エラー情報
        public static string error_mess;

        public const int INT_OK_RESP = 0;
        public const int INT_ERR_RESP = -1;

        public const string STR_OK_RESP = "0";
        public const string STR_ERR_RESP = "-1";
        public const string STR_NOT_USERENTRY_ERR_RESP = "-2";
        public const string STR_FORMAT_ERR_RESP = "-3";
        public const string STR_NOT_ENTRY_ERR_RESP = "-4";
        public const string STR_ALREADY_USED_RESP = "-5";
        public const string STR_NOT_SUPPORT_ERR_RESP = "-6";
        public const string STR_PRM_VALUE_ERR_RESP = "-7";
        public const string STR_COMMAND_NOT_SUPPORT_RESP = "-8";
        public const string STR_COMMAND_PRM_LEN_MISS_RESP = "-9";
        public const string STR_NOT_SUPPORT_PRAMS_ERR_RESP = "-10";
        public const string STR_NO_DATA = "";

        public Form1()
        {
            InitializeComponent();
            string myapp_config_file = "apps.config";    //監視用ポート番号定義ファイル

            Assembly asm = Assembly.GetExecutingAssembly();
            FileVersionInfo file_version = FileVersionInfo.GetVersionInfo(asm.Location);
            apps_version_info = String.Format("V{0}L{1}", file_version.FileMajorPart.ToString("00"), file_version.FileMinorPart.ToString("00"));

            System.Diagnostics.Process currentProcess = System.Diagnostics.Process.GetCurrentProcess();
            // リフレッシュしないとプロセスの各種情報が最新情報に更新されない
            currentProcess.Refresh();

            INTERVAL_TIME = 10000;  //初期値(10sec)
            HTTP_PORT = 8081;   //デフォルト値設定

            th_number = 0;
            error_mess = "";

            //AP用定義ファイルをチェックして情報を読み込む "apps.config"
            if (System.IO.File.Exists(myapp_config_file))
            {
                //configファイルの読み込み
                StreamReader sr = new StreamReader(myapp_config_file, Encoding.GetEncoding("Shift_JIS"));
                string s = sr.ReadToEnd();
                sr.Close();
                string[] myspps_config = s.Split(new string[] { "\r\n" }, StringSplitOptions.None);
                for (int i = 0; i < myspps_config.Length; i++)
                {
                    string[] mysppsconfig = myspps_config[i].Split(new string[] { "," }, StringSplitOptions.None);
                    if (mysppsconfig[0] == "INTERVAL_TIME")
                    { INTERVAL_TIME = int.Parse(mysppsconfig[1]); }
                    else if(mysppsconfig[0] == "TPE_FOLDER_INFO")
                    { TPE_FOLDER_INFO = mysppsconfig[1]; }
                    else if (mysppsconfig[0] == "TPE_FILE_INFO")
                    { TPE_FILE_INFO = mysppsconfig[1]; }
                    else if (mysppsconfig[0] == "CSV_FOLDER_INFO")
                    { CSV_FOLDER_INFO = mysppsconfig[1]; }
                    else if (mysppsconfig[0] == "CSV_FILE_INFO")
                    { CSV_FILE_INFO = mysppsconfig[1]; }
                    else if (mysppsconfig[0] == "HTTP_PORT")
                    { HTTP_PORT = int.Parse(mysppsconfig[1]); }
                }
            }
            else
            {
                MessageBox.Show("『" + myapp_config_file + "』は存在しません。");
                INTERVAL_TIME = 10000;
                HTTP_PORT = 8081;

            }
            hMain = new Thread(new ThreadStart(httpserver_task));   //isense 2017/05/22 追加
            hMain.Start();  //isense 2017/05/22 追加
            logger.Info("httpserver_task start");


            //メインプログラムの開始
            logger.Info("tepra_print Start "+ apps_version_info);  //ログ

        }

        private void 閉じるToolStripMenuItem_Click(object sender, EventArgs e)
        {
            //メッセージボックスを表示する
            DialogResult result = MessageBox.Show("終了しますか?",
                "確認",
                MessageBoxButtons.YesNo,
                MessageBoxIcon.Exclamation,
                MessageBoxDefaultButton.Button2);
            if (result == DialogResult.Yes)
            {

                //this.Close();
                logger.Info("tepra_print exit");  //ログ
                Application.Exit();
            }
        }

        private void timer1_Tick(object sender, EventArgs e)
        {

        }
        //手動実行
        private void button1_Click(object sender, EventArgs e)
        {
            if(textBox1.Text == "" || textBox2.Text == "")
            {
                MessageBox.Show("情報が未指定です。", "確認");
                return;
            }
            string csv_info = textBox1.Text + "," + textBox2.Text+","+'"'+ textBox1.Text + "," + textBox2.Text+'"';
            string rs = create_csv_file(csv_info);
            if(rs == STR_NO_DATA)
            {
                logger.Info("print_exec_to_TEPRA 処理中断");  //ログ
                return;
            }
            logger.Info("print_exec_to_TEPRA");  //ログ
            int rsp = print_exec_to_TEPRA(CSV_FILE_INFO);
            logger.Info("print_exec_to_TEPRA resp="+ rsp.ToString());  //ログ
        }
        //指定情報csv_dataで
        //ファイルCSV_FOLDER_INFO+CSV_FILE_INFOに書き込む
        public static string create_csv_file(string csv_data)
        {
            string path = CSV_FOLDER_INFO+CSV_FILE_INFO;
            try {
                System.IO.StreamWriter textFile;

                textFile = new System.IO.StreamWriter(path,false,System.Text.Encoding.GetEncoding("shift_jis"));
                textFile.WriteLine(csv_data);
                logger.Info("output:"+ path+" data="+csv_data);
                textFile.Close();
                return CSV_FILE_INFO;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "通知");
                return STR_NO_DATA;
            }
        }
        // TEPRA印刷(SPC10.exeを実行)
        // csv_data_info:csvファイル フォルダ情報を含めたCSVファイル名
        public static int print_exec_to_TEPRA(string csv_file_info)
        {
            //Processクラスのインスタンスを作成
            Process process = new Process();
            //起動したい外部アプリの情報を設定
            ProcessStartInfo psInfo = new ProcessStartInfo();
            psInfo.FileName = SPC10_CMD; // 実行するファイル
            string arguments_info  = "/p " + '"' + TPE_FOLDER_INFO + TPE_FILE_INFO + "," + CSV_FOLDER_INFO + csv_file_info + ",1" + '"';   //引数 tpe,csv,枚数
            if (COMMAND_OPTION != "")
            {
                arguments_info += ","+COMMAND_OPTION;
            }
            psInfo.Arguments = arguments_info;
            psInfo.CreateNoWindow = false; // コンソール・ウィンドウを開かない
            psInfo.UseShellExecute = true; // シェル機能を使用しない
            logger.Info("FileName ="+psInfo.FileName);
            logger.Info("Arguments = " + psInfo.Arguments);
            process = Process.Start(psInfo);
            //外部アプリの終了を待機する
            process.WaitForExit();
            return process.ExitCode;
        }

        private void httpserver_task()
        {
            try
            {
                //var port = Properties.Settings.Default.HTTP_SERVER_PORT;
                var port = HTTP_PORT;
                var httpserver = new HttpServer(port);
                httpserver.Listen();

                logger.Info("httpserver_task exit");
            }
            catch (Exception ee)
            {
                logger.Info("httpserver_task処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");
            }
        }
        private void StopHttpServer()
        {
            try
            {
                SendHttpServerStopMessage();


                var th_state = hMain.ThreadState.ToString();

                logger.Info("status=" + th_state);
                logger.Info("サーバアプリ終了しました。");
            }
            catch (Exception ee)
            {
                logger.Info("サーバアプリの終了処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");
            }
        }
        private void SendHttpServerStopMessage()
        {
            try
            {
                var port = HTTP_PORT;
                var sendMsg = "exit";

                using (var client = new WebClient())
                {
                    var res = client.UploadString($@"http://localhost:{port}", sendMsg);
                }
            }
            catch { }
        }

    }
}

HttpServer.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Web;
using System.Threading.Tasks;

namespace tepra_print
{
    public class HttpServer
    {
        private Encoding EncodintType = Encoding.UTF8;
        private HttpListener Listener = null;
        private int Port = int.MinValue;
        private string Host;
        private bool flag = false;

        public HttpServer(int port = 80)
        {
            Port = port;
            Host = "*";
            Listener = new HttpListener();
        }

        public void Listen()
        {
            Listener.Prefixes.Clear();
            Listener.Prefixes.Add($@"http://localhost:{Port}/");
            Listener.Prefixes.Add($@"http://{Host}:{Port}/");
            Listener.Start();

            try
            {
                while (true)
                {
                    var context = Listener.GetContext();

                    if (flag) break;

                    try
                    {
                        var thread = new Thread(DoWork);
                        Form1.logger.Info("[httpListener]thread.Start");
                        thread.Start(context);
                        Form1.logger.Info("accept httpListener");
                    }
                    catch
                    {
                        var response = context.Response;
                        response.StatusCode = 500;
                        response.Close();
                    }
                }

                Form1.logger.Info("exit httpListener");
            }
            catch (Exception ee)
            {
                Form1.logger.Info("[httpListener]Listen処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");
            }
            finally
            {
                Listener.Stop();
                Listener.Close();
                Listener = null;
            }
        }

        private void DoWork(object obj)
        {
            Form1.th_number += 1;
            if (Form1.th_number > 10000)
                Form1.th_number = 1;

            long mythnum = Form1.th_number;
            string code_info = "";
            string label_info = "";

            var context = (HttpListenerContext)obj;
            var request = context.Request;
            var response = context.Response;
            string request_type = request.HttpMethod;   // GETorPOST
            Form1.logger.Info(request.RawUrl);
            try
            {

                // リクエストされたURLからファイルのパスを求める
                string path = request.RawUrl.Replace("/", "\\");
                var localpath = context.Request.Url.LocalPath;
                if (localpath == "/")
                {
                    //HttpUtility使うには、Microsoft.AspNet.WebApi nugetでインストール
                    var queryDictionary = HttpUtility.ParseQueryString(context.Request.Url.Query);
                    code_info = queryDictionary.Get("code");
                    label_info = queryDictionary.Get("label");
                    //TEPRA印刷実行
                    string csv_info = code_info + "," + label_info + "," + '"' + code_info + "," + label_info + '"';
                    string rs = Form1.create_csv_file(csv_info);
                    if (rs == Form1.STR_NO_DATA)
                    {
                        Form1.logger.Info("print_exec_to_TEPRA 処理中断");  //ログ
                        return;
                    }
                    Form1.logger.Info("print_exec_to_TEPRA");  //ログ
                    int rsp = Form1.print_exec_to_TEPRA(Form1.CSV_FILE_INFO);
                    Form1.logger.Info("print_exec_to_TEPRA resp=" + rsp.ToString());  //ログ

                }


                Form1.error_mess = "";
                string resp_mess = "";

                try
                {
                    if (localpath != "/")
                    {
                        resp_mess = Form1.STR_OK_RESP + '\n';
                    }
                    else if(code_info == "")
                    {
                        Form1.logger.Info("受信フォーマットエラー uri=" + request.RawUrl);
                        resp_mess = Form1.STR_FORMAT_ERR_RESP + '\n';
                    }
                    else
                    {
                        Form1.logger.Info("code=" + code_info + " label=" + label_info);
                        resp_mess = Form1.STR_OK_RESP + '\n';
                    }

                    var sendBytes = EncodintType.GetBytes(resp_mess);

                    if (Form1.error_mess != "")
                    {
                        Form1.logger.Info("エラー情報:" + Form1.error_mess);
                    }

 
                    response.OutputStream.Write(sendBytes, 0, sendBytes.Length);
                    response.StatusCode = 200;
                    response.Close();
                }
                catch (Exception ee)
                {
                    Form1.logger.Info("DoWork処理が、異常終了しました。\n" + ee.StackTrace + ee.Message + "\n");

                    resp_mess = Form1.STR_ERR_RESP + '\n';
                    byte[] sendBytes = EncodintType.GetBytes(resp_mess);
                    response.OutputStream.Write(sendBytes, 0, sendBytes.Length);
                    response.StatusCode = 500;
                    response.Close();
                }
            }
            finally
            {
                //Form1.logger.Info("DoWork処理が、終了しました。\n");
            }
        }
    }
}

tpeファイルは、bihin_12.tpeで12mmテープを使用しています。
PC(Windows10)は、8081のポート開放を実施しておくことで、Linuxサーバ上のPHP言語からcurlコマンドでWindows C#アプリニhttpでリクエスト送信して、ラベル印刷できます。
今回は、ここまでの紹介です。

この記事が気に入ったらサポートをしてみませんか?