
スマホでQRコードを読み取り備品管理を
前回までの記事(No.024)で備品管理する保管先と備品情報の情報管理が実装できましたので、今回は、QRコードを読み取って、備品管理に活用する機能を紹介します。今回、紹介する内容は、備品情報を保管する保管先情報を手で指定する方法でなく、保管先に貼ってあるQRコードをスマホで読み込み、保管先として設定する機能です。
バーコードリーダを使用せず、スマホのカメラでQRコードを読み取る機能を実装します。JavascriptでQRコードを解析する機能はいくつかネットで紹介されていますが、今回は、jsQR.jsというライブラリを使用します。
jsQR.jsを利用した事例もネットに紹介されています。以下の記事を参考にしています。
事前にjsQR.jsというライブラリをダウンロードして、Webサーバに格納しておきます。今回は、以下のフォルダに格納しています。
/var/www/html/webix01/commonlib/jsQR
従って、ライブラリを使用するときには、以下のように記述して組み込みます。
<script src="/webix01/commonlib/jsQR/jsQR.js"></script>
また、カメラを使用してリアルタイムにQRコードを撮影し、コード解析できたときに処理を中断する機能などを共通ライブラリとして定義します。
以下のように動画を表示するエリアを定義して、描画します。
<div id="wrapper" style="visibility: hidden; border: 2px solid #099;">
<video id="video" autoplay muted playsinline></video>
<canvas id="camera-canvas"></canvas>
<canvas id="rect-canvas"></canvas>
</div>
webixライブラリも同時に使用するため、webixで描画する画像を表示する場所も事前に宣言しておきます。
<div id="webix01"></div>
webixでは、以下のように描画先をcontainerで指定することもできます。
webix.ui({
view:"form",
id: "EQ0020_form",
container:"webix01",
resize:true,
elements:form_collection ,
});
以下は、QRコード解析用の共通ライブラリです。
EQ0021_QRcode_common.php
// EQ0021_QRcode_common.php
// QRコード読み取りライブラリ
// 必要なライブラリ
// メイン側で/commonlib/jsQR/jsQR.jsが記述されていること
// httpsでアクセスすること
//
// 参考URL
// url https://zenn.dev/sdkfz181tiger/articles/096dfb74d485db
var return_filed_id = ""; //バーコードを読み取った後に設定するwebixコンポーネントのID
// 音声データ(正常時とエラー時の音情報
var success_base64 = ""; //音源の定義を実際にはここに記述します。
var error_base64 = ""; //音源の定義を実際にはここに記述します。
var success_sound = new Audio("data:audio/wav;base64," + success_base64);
var error_sound = new Audio("data:audio/wav;base64," + error_base64);
var video_playObj;
//QRコードスキャンのインターバル(ミリ秒)
const INTERVAL500 = 500;
let reserveEnd=null;
//スマホに表示するWebカメラ画面サイズ
const videoSize = {
w: 640,
h: 480
};
// Webカメラの起動
// メインソースで事前に対象elementが宣言されていることwrapper,video
const warpper = document.getElementById("wrapper");
const video = document.getElementById("video");
let contentWidth;
let contentHeight;
let scanningCnt = 0;
blnCameraInit = false;
//カメラ使用の許可ダイアログが表示される
//マイクはオフ, カメラの設定 背面カメラ videoSize:640×480
const media = navigator.mediaDevices.getUserMedia(
{"audio":false,"video":{facingMode:"environment","width":{"ideal": videoSize.w},"height":{"ideal": videoSize.h}}}
)
//カメラと連携が取れた場合
.then((stream) => {
video.srcObject = stream;
blnCameraInit = true;
video.onloadeddata = () => {
contentWidth = video.clientWidth;
contentHeight = video.clientHeight;
}
}).catch((err) => {
switch(err.message) {
case "Requested device not found":
webix.message({type:"error",text:"カメラ取得に失敗しました"});
break;
default:
webix.message({type:"error",text:err.message});
}
});
// カメラ映像のキャンバス表示
const cvs = document.getElementById('camera-canvas');
const ctx = cvs.getContext('2d');
ctx.willReadFrequently = true;
const canvasUpdate = () => {
cvs.width = contentWidth;
cvs.height = contentHeight;
ctx.drawImage(video, 0, 0, contentWidth, contentHeight);
requestAnimationFrame(canvasUpdate);
}
//QRコードが読めたときにコールする関数式定義
// 以下は、初期値のダミー関数式
var qrcode_callback_func = function(){
console.log("qrcode_callback_func");
}
// QRコードの検出
const rectCvs = document.getElementById('rect-canvas');
const rectCtx = rectCvs.getContext('2d');
const checkImage = function(){
// imageDataを作る
const imageData = ctx.getImageData(0, 0, contentWidth, contentHeight);
// jsQRに渡す
const code = jsQR(imageData.data, contentWidth, contentHeight);
if(scanningCnt== -1){
//エラー時は、処理を中断
if(return_filed_id != "") $$(return_filed_id).setValue("");
return ;
}
// 検出結果に合わせて処理を実施(codeが有効ならライブラリでバーコード情報検出)
if (code) {
drawRect(code.location); //見つけたエリア西角を描画
if(return_filed_id != "") $$(return_filed_id).setValue(code.data); //読み取った情報をreturn_filed_idにセット
video.load(); //要素をリセットする、新たなリソースを選択しロードを開始する
warpper.style.visibility="hidden"; //video停止モードにセット
if(qrcode_callback_func){ //関数式が定義されていたら、コール
qrcode_callback_func();
}
return ;
} else {
rectCtx.clearRect(0, 0, contentWidth, contentHeight);
}
setTimeout(()=>{ checkImage() }, INTERVAL500);
}
function scanning() {
//スキャン本体
canvasUpdate();
checkImage();
}
// 四辺形の描画
const drawRect = (location) => {
rectCvs.width = contentWidth;
rectCvs.height = contentHeight;
drawLine(location.topLeftCorner, location.topRightCorner);
drawLine(location.topRightCorner, location.bottomRightCorner);
drawLine(location.bottomRightCorner, location.bottomLeftCorner);
drawLine(location.bottomLeftCorner, location.topLeftCorner)
}
// 線の描画
const drawLine = (begin, end) => {
rectCtx.lineWidth = 4;
rectCtx.strokeStyle = "#F00";
rectCtx.beginPath();
rectCtx.moveTo(begin.x, begin.y);
rectCtx.lineTo(end.x, end.y);
rectCtx.stroke();
}
function play_video(){
video_playObj = video.play();
if(video_playObj){
video_playObj.then(() => {
// videoの起動成功時
return;
}).catch((e) => {
// video loading failed
console.log("video loading failed");
return;
});
}
}
//QRコードスキャン開始と停止のトグル
function toggleScan() {
if(warpper.style.visibility=="visible") { //video起動モードなら停止する
scanEnd();
} else {
scanStart();
}
}
//QRコードスキャン開始
function scanStart() {
play_video();
video.load(); //要素をリセットする、新たなリソースを選択しロードを開始する
scanningCnt = 0;
warpper.style.visibility="visible"; //video起動モードにセット
if (blnCameraInit==false) {
reserveEnd = setTimeout(() => {
warpper.style.visibility="hidden";//video停止モードにセット
}, 3000);
}
else {
play_video()
contentWidth = video.clientWidth;
contentHeight = video.clientHeight;
setTimeout(scanning,0);
}
}
//QRコードスキャン停止
function scanEnd() {
if (reserveEnd != null) {
clearTimeout(reserveEnd);
}
scanningCnt=-1;
video.pause();
warpper.style.visibility="hidden"; //video停止モードにセット
}
QRコードを読み込むには動画を画面に表示する必要がありますが、jsQR.jsを使用するときには、動画描画位置をHTMLタグで指定する必要があります。
一方、業務アプリは、webixライブラリで実装していて、詳細画面は、windowコンポーネントで表示する実装になってしまい、windowコンポーネントにHTMLのタグで動画描画位置を指定できないので、うまく実装できない課題が判明しました。動画を表示する画面と一覧から詳細画面を表示する機能を同じページに実装するには、スマホ画面デザイン上、うまく実装できそうにないため、以下の方式を採用することにしました。一覧画面+詳細画面とQRコード解析用画面は、別ページで表示し、ページ間で情報連携する。つまり、QRコードを読み取った画面(ページ)から一覧+詳細画面に情報を転送する方法をとりました。ページ間の情報転送には、Local Storage Eventsを利用します。以下の記事を参考にして実装しました。
受信側で、イベント待ちを定義し、送信側で情報を設定すると、イベント待ちしているページで情報を受信できます。情報は、連想配列を文字列に変換して転送しました。
受信側のソース例です。

送信側のソース例です。

実際に実装した詳細画面です。

編集モードにしたときに、格納先QRボタンが有効になるように実装しています。格納先QRを押下すると、別ページでQRコード読みだし用の画面が新規に開きます。(初めてカメラ操作をするときには、許可するポップアップ表示が出ます。)QRコードを正しく読み込めたかを音声でも表現するために自動音声出力機能も実装しましたが、音声を自動出力するためには、一度は、手動で音声出力する必要があり、音声ONボタンも実装しています。実際のQRコード読み出し操作は、読取りボタン押下すると、カメラが表示されます。

以下のように印刷したQRラベルにフォーカスを合わせると、自動的に、QRコードを解析し、画面が閉じられます。

読み込んだQRコード(今回は、保管先のコードなので、該当データベースを検索して、保管先名称も画面に表示)から必要な情報をとりだして詳細画面に表示しています。格納場所コードを手動で選択しなくてもQRコードを読み込んで設定が可能となります。

参考までに、QRコードを読み込む画面のソースを紹介します。
EQ0020_QRcode_read.php このソースは、上記の共通ライブラリEQ0021_QRcode_common.phpを使用しています。
<?php
//EQ0020_QRcode_read.php
// ref https://qiita.com/U_sagi/items/12cc39487a863e0136a0
// ref https://zenn.dev/sdkfz181tiger/articles/096dfb74d485db
// ref https://nanbu.marune205.net/2021/12/barcode-scan-quaggajs.html?m=1
// quagga2
// ref https://github.com/ericblade/quagga2
$TITLE_INFO ="QRcode";
$VER_INFO ="V01L01";
$JOB_INFO = "EQ0020";
$myfilename = basename(__FILE__); //自分自身のファイル名取得
define('SUB_FOLDER','/webix01'); //サブフォルダを指定したURL
define('ROOT_PATH','/var/www/html/webix01'); //ソースを保存しているパス(動作環境に応じて記述する必要あり)
$userid = '';
$logheader = 'userid='.$userid.', '.$myfilename.':';//ログ出力時のヘッダー情報(自ファイル名,ログインIDを付与)
error_log($logheader.' EQ0020_QRcode_read.php GET');
if($_SERVER["REQUEST_METHOD"] != "GET"){
//GET以外ははじく
error_log($logheader.' REQUEST_METHOD no GET');
header("HTTP/1.0 404 Not Found");
return;
}
$accesskey = 0;
$userclient = 'smd';
$mypermission = 0;
if(isset($_GET['accesskey'])){
if(is_numeric($_GET['accesskey'])){
$accesskey = intval($_GET['accesskey']);
}
}
else{
error_log($logheader.' accesskey not found');
header("HTTP/1.0 404 Not Found");
exit;
}
if(isset($_GET['userid'])){
$userid = $_GET['userid'];
}
else{
error_log($logheader.' userid not found');
header("HTTP/1.0 404 Not Found");
exit;
}
if(isset($_GET['userclient'])){
$userclient = $_GET['userclient'];
}
if(isset($_GET['permission'])){
if(is_numeric($_GET['permission'])){
$mypermission = intval($_GET['permission']);
}
}
$logheader = 'userid='.$userid.', '.$myfilename.':';//ログ出力時のヘッダー情報(自ファイル名,ログインIDを付与)
include('../../commonlib/svr_common_lib_v2.php'); //
$config_obj = get_config_obj();
//アクセスキーチェック
if(Chk_AccessKey($accesskey)){
}
else{
error_log($logheader.' accesskey check error');
header("HTTP/1.0 404 Not Found");
exit;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/webix-pro1021/css/materialdesignicons.min.css" type="text/css" charset="utf-8">
<script src="/webix-pro1021/webix.js" type="text/javascript" charset="utf-8"></script>
<link href="/webix-pro1021/skins/compact.css?<?php echo date('Ymd-H'); ?>" rel="stylesheet" type="text/css">
<link href="<?php echo SUB_FOLDER; ?>/commonlib/webix_custom_css.css?<?php echo date('Ymd-H'); ?>" rel="stylesheet" type="text/css">
<link rel="icon" href="<?php echo SUB_FOLDER; ?>/image/webix_64.ico">
<script src="<?php echo SUB_FOLDER; ?>/commonlib/object-assign.js"></script>
<script src="<?php echo SUB_FOLDER; ?>/commonlib/moment-with-locales.js"></script>
<script src="<?php echo SUB_FOLDER; ?>/commonlib/webix_common_lib.js"></script>
<script src="<?php echo SUB_FOLDER; ?>/commonlib/jsQR/jsQR.js"></script>
<style>
#wrapper{
position: relative;
}
#video{
position: absolute;
top: 0px;
left: 0px;
visibility: hidden;
}
#camera-canvas{
position: absolute;
top: 0px;
left: 0px;
z-index: 50;
}
#rect-canvas{
position: absolute;
top: 0px;
left: 0px;
z-index: 100;
}
</style>
<title><?php echo $TITLE_INFO.' ('.$VER_INFO.')' ?></title>
</head>
<body>
<div id="webix01"></div>
<div id="wrapper" style="visibility: hidden; border: 2px solid #099;">
<video id="video" autoplay muted playsinline></video>
<canvas id="camera-canvas"></canvas>
<canvas id="rect-canvas"></canvas>
</div>
<script type="text/javascript" charset="utf-8">
webix.i18n.setLocale("ja-JP");
webix.ui.fullScreen();
<?php
include('../../commonlib/CM0010_goto_menu_action.php');
include('../../commonlib/CM0030_sendprm_set_request.php');
include('../../commonlib/CM0050_open_menucmd.php');
include_once('../../commonlib/CM0060_common_validate_check.php');
include_once('../../commonlib/CM0080_screen_control.php');
echo ' var my_permission ='.$mypermission.";\n";
if(isset($_GET['open_mode'])){
$open_mode = $_GET['open_mode'];
if($open_mode == '_blank'){
echo ' var menu_btn_name = "閉じる";'."\n";
}
else{
echo ' var menu_btn_name = "メニュー";'."\n";
}
}
else{
echo ' var menu_btn_name = "メニュー";'."\n";
}
include_once('EQ0021_QRcode_common.php');
?>
var my_local_session = webix.storage.local.get('login');
qrcode_callback_func = function(){
var qrcode_info = $$(return_filed_id).getValue();
if(qrcode_info == ""){
//読み取ったデータが""なので、処理中断
return false;
}
else{
$$("EQ0020_form").showProgress({hide:false});
webix.delay(function(){
var access_key = Get_AccessKey();
send_prm = Prepare_send_prm(my_local_session,access_key);
if(qrcode_info.substr(0,2) == "LC") {
send_prm.qrcode = qrcode_info;
var xhr =webix.ajax().sync().get("<?php echo SUB_FOLDER; ?>/rest_api/EQ0020/EQ0022_getlocationinfo.php", send_prm);
var resp = JSON.parse(xhr.responseText);
$$("EQ0020_form").hideProgress();
if(resp.resp =="ok"){
if(resp.name == ""){
error_sound.play();
webix.message({type:"debug",text:"該当なし:"+qrcode_info});
}
success_sound.play();
var data_list ={};
data_list.code =qrcode_info
data_list.name =resp.name;
var qrcode_infos = JSON.stringify(data_list)
window.localStorage.setItem("qrcode_infos", qrcode_infos);
Goto_Menu();
}
else{
error_sound.play();
webix.message({type:"error",text:qrcode_info+"の情報取得できませんでした。"});
}
}
else if(qrcode_info.substr(0,2) == "EQ") {
send_prm.qrcode = qrcode_info;
var xhr =webix.ajax().sync().get("<?php echo SUB_FOLDER; ?>/rest_api/EQ0020/EQ0023_getequipmentinfo.php", send_prm);
var resp = JSON.parse(xhr.responseText);
$$("EQ0020_form").hideProgress();
if(resp.resp =="ok"){
if(resp.name == ""){
error_sound.play();
webix.message({type:"debug",text:"該当なし:"+qrcode_info});
}
success_sound.play();
var data_list ={};
data_list.code =qrcode_info
data_list.name =resp.name;
var qrcode_infos = JSON.stringify(data_list)
window.localStorage.setItem("qrcode_infos", qrcode_infos);
Goto_Menu();
}
else{
error_sound.play();
webix.message({type:"error",text:qrcode_info+"の情報取得できませんでした。"});
}
}
else{
$$("EQ0020_form").hideProgress();
error_sound.play();
var data_list ={};
data_list.code =qrcode_info
data_list.name =""
var qrcode_infos = JSON.stringify(data_list)
window.localStorage.setItem("qrcode_infos", qrcode_infos);
Goto_Menu();
}
return true;
},null, null, 500);
}
}
function status_info(status_id){
var status_array ={"0":"未定","1":"保管中","2":"使用中","3":"廃棄"};
if(status_id == "1"){
var css = "<span style='color:#0000FF;font-weight:bold;'>";
return css+ status_array[status_id]+"</span>";
}
else{
return status_array[status_id];
}
}
//検索条件フォーム構成リスト
var form_collection = [
{ margin:5, cols:[
{view:"text" ,width:300,labelWidth:100,label: 'QRコード情報',id:"EQ0020_qrcode",name:"EQ0020_qrcode",labelAlign:"right" },
]
},
{ margin:5, cols:[
{ view:"button", label:"音声ON",id:"sound_btn",width:110,align:"right" ,
click:function(){
success_sound.play(); //一度、手動で音を出さないと自動では、再生されないので注意(ブラウザ仕様)
}
},
{ view:"button", label:"読取り/停止",id:"scan_btn",width:110,align:"right" ,
click:function(){
$$(return_filed_id).setValue("");
toggleScan();
}
}
]
},
{ margin:5, cols:[
{ view:"button", label:"手入力",id:"manual_btn",width:110,align:"right" ,
click:function(){
var qrcode = $$("EQ0020_qrcode").getValue();
if(qrcode == ""){
webix.alert("QRコード情報未設定です。");
return;
}
qrcode_callback_func();
}
},
{ view:"button", label:"クリア",id:"clear_btn",width:110,align:"right" ,
click:function(){
$$(return_filed_id).setValue("");
$$("EQ0020_qrcode").setValue("");
}
},
{ view:"button", value: menu_btn_name, align:"center", width: 110, css:"menu",
click:function(){
Goto_Menu();
}
},
]
},
];
webix.ui({
view:"form",
id: "EQ0020_form",
container:"webix01",
resize:true,
elements:form_collection , //フォームの構成要素を指定
});
webix.extend($$("EQ0020_form"), webix.ProgressBar);
return_filed_id = "EQ0020_qrcode";
</script>
</body>
</html>
複数ページ間の通信ができるようにしたことで、webixで作成する業務アプリにQRコード解析画面を実装することが可能となりました。
次回は、実際の備品を保管場所から取り出したり、保管場所に格納するときの操作用画面を紹介します。