LoginSignup
3
0

IBM i 上でNode.jsのWebアプリを動かす その4 (QR読み取りとアプリ詳細)

Posted at

はじめに

「IBM i 上でNode.jsのWebアプリを動かす」3つ目の記事です。

  • 1つ目の記事はこちら
    IBM i 上での Node.js の動かし方を簡単に紹介しています。

  • 2つ目の記事はこちら
    環境情報や作成したWebアプリの概要はこちらをご覧ください。

作成したコードはこちらにおいています。

この記事の中では、こちらのアプリを「QRコードアプリ」と記載しています。
Node.jsでのWebアプリ制作に関しては手探りで行っていますので、荒いところがあるかもしれません。

JavaScriptでのQRコード読み取り

QRコードの読み取りには、こちらで公開されている jQR を利用しています。

また、こちらの記事を参考にさせていただいております。

jsQRの使い方については、上記2つの記事を参考にしていただければと思います。
ここでは、QRコードアプリを作成した際に詰まった部分について記述します。

当初は、QRコード読み取り用のJavaScriptを別のファイルとして用意していました。
ただ、express, ejs の組み合わせで、うまくデータの受け渡しができず、ejsファイル内に JavaScript をベタがきしています。
良くないとは思いつつ、自分の技術力不足のためこの形になっています。

httpsでのアクセス

カメラをブラウザ上に表示する際、環境によっては https でのアクセスが必須になるようです。
今回はこれに該当したため、自己署名証明書を作って簡易的にhttpsアクセスが出来るよう設定しています。
httpsでアクセスができない場合、カメラが起動しない状態となります。

httpsアクセスに関しては、こちらを参考にしています。

まずは、自己署名証明書を作成します。
QRコードアプリでは、key ディレクトリを作成して、そこに証明書のファイルを配置しています。
下記のように実行すると、証明書が作成できます。

key # openssl genrsa -out server_key.pem 2048
key # openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Tokyo/L=Musashino-shi/O=Foo/OU=Bar/CN=foo.bar.com"
key # openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Certificate request self-signature ok
subject=C = JP, ST = Tokyo, L = Musashino-shi, O = Foo, OU = Bar, CN = foo.bar.com

これを前回の記事で作成したExpress + EJSの簡易的なWebアプリを変更して https でアクセス出来るようにします。
前回作成したコードは、こちらでした。

express-test # cat index.js 
const express = require('express');

const app = express();

app.set("view engine", "ejs");

const hostname = '<サーバーのIPアドレス>';
const port = 9443;

app.get('/', (req, res) => {
  res.render('index', {data: 'jsから渡したデータ'});
});

app.listen(port, () => console.log(`Listening on port ${port}!`));

このコードを以下のように書き換えています。

  1. パッケージ fs の追加
  2. 証明書の読み込みとhttpsサーバーの作成
  3. appではなくserverでの起動に変更
express-test # cat index.js 
const express = require('express');
var fs = require('fs'); //追加 1

const app = express();

app.set("view engine", "ejs");

//追加 2
const server = require('https').createServer(
  {
    key: fs.readFileSync('./key/server_key.pem'),
    cert: fs.readFileSync('./key/server_crt.pem'),
  },
  app
);

const hostname = '<サーバーのIPアドレス>';
const port = 9443;

app.get('/', (req, res) => {
  res.render('index', {data: 'jsから渡したデータ'});
});

server.listen(port, () => console.log(`Listening on port ${port}!`)); //変更 3

この変更を行うことで、httpsでのアクセスが可能になります。

ブラウザでアクセスすると、最初は警告画面が出るので、詳細設定を開いた上でページにアクセスします。

image.png

アクセスすると、httpsで表示されていることがわかります。

image.png

IBM i 上でjsQRを動かすときには、このhttpsの設定が必要でした。

QRコードアプリ詳細

ここまで、QRコードアプリを構成しているパーツについて書いてきました。
それでは、実際に作成したアプリの動きを見ていきます。
画面については、あまりカスタマイズしてないためシンプルな見た目となってます。

アクセスすると、このような画面が表示されます。
index.ejs が表示されている状態となり、jqQRがブラウザ側で動いている状態です。
黒色で表示されているのが、カメラ画面になります。

index.ejs
index.ejs
<!DOCTYPE html>
<html lang="jp">
<head>
    <%- include('./partials/head'); %>
    <link rel="stylesheet" href="css/index.css">
</head>
<body class="container">

<header>
    <%- include('./partials/header'); %>
</header>

<main>
    <div id="loadingMessage"> カメラが有効になっていません</div>
    <canvas id="canvas" hidden></canvas>
    <div id="outputs" hidden>
      <div id="outputMessage">QRコード検出なし</div>
      <div hidden><b>登録番号:</b> <span id="outputData"></span></div>
      <div hidden><b></b> <span id="result"></span></div>
    </div>
  
    <div id="outputs" hidden>
        <button type="button" class="btn btn-primary" onclick="yes()">Yes</button>
        <button type="button" class="btn btn-secondary" onclick="no()">No</button>
    </div>

</main>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>

<script type="text/javascript">
var video = document.createElement("video");
var canvasElement = document.getElementById("canvas");
var canvas = canvasElement.getContext("2d");
var loadingMessage = document.getElementById("loadingMessage");
var outputContainer = document.getElementById("outputs");
var outputMessage = document.getElementById("outputMessage");
var outputData = document.getElementById("outputData");
var result = document.getElementById("result");
var requestID;

function drawLine(begin, end, color) {
  canvas.beginPath();
  canvas.moveTo(begin.x, begin.y);
  canvas.lineTo(end.x, end.y);
  canvas.lineWidth = 4;
  canvas.strokeStyle = color;
  canvas.stroke();
}

// Use facingMode: environment to attemt to get the front camera on phones
navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then(function(stream) {
  video.srcObject = stream;
  video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
  video.play();
  requestAnimationFrame(tick);
});


var qr_flag = JSON.parse("<%= flag %>".replace(/&#34;/g,'"'));
var update_flag = JSON.parse("<%= update_flag %>".replace(/&#34;/g,'"'));

if (update_flag == 1){
  outputMessage.innerText = '入室処理が完了しました。';
} else if (update_flag == 2){
  outputMessage.innerText = '退室処理が完了しました。';
} else if (update_flag == 3) {
  outputMessage.innerText = 'すでに処理が完了しているため、変更されませんでした。';
}

function tick() {
  loadingMessage.innerText = "Loading video..."
  if (video.readyState === video.HAVE_ENOUGH_DATA) {
    loadingMessage.hidden = true;
    canvasElement.hidden = false;
    outputContainer.hidden = false;

    canvasElement.height = video.videoHeight;
    canvasElement.width = video.videoWidth;
    canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
    var imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
    var code = jsQR(imageData.data, imageData.width, imageData.height, {
      inversionAttempts: "dontInvert",
    });

      if (code) {
        drawLine(code.location.topLeftCorner, code.location.topRightCorner, "#FF3B58");
        drawLine(code.location.topRightCorner, code.location.bottomRightCorner, "#FF3B58");
        drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, "#FF3B58");
        drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, "#FF3B58");
        outputMessage.hidden = true;
        outputData.parentElement.hidden = false;
        outputData.innerText = code.data;

        if (code.data) {
          post("/qrid", {id:code.data});
          sleep(300);
        } else {
          result.parentElement.hidden = false;
        }

      } else {
        outputMessage.hidden = false;
        outputData.parentElement.hidden = true;
        result.parentElement.hidden = true;
      }
    }
    requstID = requestAnimationFrame(tick);
}

function post(path, params, method='post') {
  const form = document.createElement('form');
  form.method = method;
  form.action = path;

  for (const key in params) {
    if (params.hasOwnProperty(key)) {
      const hiddenField = document.createElement('input');
      hiddenField.type = 'hidden';
      hiddenField.name = key;
      hiddenField.value = params[key];

      form.appendChild(hiddenField);
    }
  }

  document.body.appendChild(form);
  form.submit();
}

function sleep(waitMsec) {
  var startMsec = new Date();

  while (new Date() - startMsec < waitMsec);
}
</script>


</body>
</html>

スクリーンショット 2023-12-14 19.24.09.png

QRコードを写すと、一瞬QRコードの部分が赤枠で囲われ、次の画面に移ります。
読み取りが完了すると、読み取ったIDをPOSTリクエストでサーバー側に送信します。

スクリーンショット 2023-12-14 19.24.17.png

QRコードを読み取り、IBM i のDBに一致するIDが存在する場合は、処理を行うかどうかの選択肢が表示されます。
POSTリクエストを受け付けると、サーバー側では、idb-connect を介してデータが存在するかどうか確認するためのSQLが実行されています。

app.post('/qrid', (req, res) => {
  if (req.body.id != null){
    const sql = `SELECT * FROM ACCESS.ACCESSTABLE WHERE QRID = ${req.body.id} LIMIT 1`;
    const connection = new dbconn();
    connection.conn('*LOCAL');
  
    const statement = new dbstmt(connection);
    const result = statement.execSync(sql);

    let qrid = JSON.stringify(result[0].QRID);
    let name = JSON.stringify(result[0].NAME);
    let status = JSON.stringify(result[0].STATUS);

    statement.close();
    connection.disconn();
    connection.close();

    res.render('submitqr.ejs', {qrid : qrid, name : name, status : status, flag : 1});
  }
});

スクリーンショット 2023-12-14 19.24.22.png

はい を選択すると、POSTリクエストが送信され、サーバー側ではDBのデータ更新が行われます。
今回のアプリだと、予約状態から入室状態に、入室状態から退出状態に STATUS を変更します。

入退出のDB更新部分
app.post("/submit",(req,res)=>{
  let update = 0;

  const connection = new dbconn();
  connection.conn('*LOCAL');
  const statement = new dbstmt(connection);

  const sql = `SELECT * FROM ACCESS.ACCESSTABLE WHERE QRID = ${req.body.id} LIMIT 1`;
  const result = statement.execSync(sql);

  let qrid = JSON.stringify(result[0].QRID).slice( 1, -1 );
  let status = JSON.stringify(result[0].STATUS).slice( 1, -1 );

  const statement_update = new dbstmt(connection); 

  if (status == 'schedule'){
    const sql_up = `UPDATE ACCESS.ACCESSTABLE SET STATUS = 'stay' WHERE QRID = ${qrid}`;
    const result = statement_update.execSync(sql_up);
    update = 1;
  } else if (status == 'stay'){
    const sql_up = `UPDATE ACCESS.ACCESSTABLE SET STATUS = 'leave' WHERE QRID = + ${qrid}`;
    const result = statement_update.execSync(sql_up);
    update = 2;
  } else {
    update = 3;
  } 

  statement.close();
  statement_update.close();
  connection.disconn(); 
  connection.close();

  res.render('index.ejs', {update_flag : update, flag : 0});
})

スクリーンショット 2023-12-14 19.24.30.png

一連の流れは以上になります。

アプリ全体のコードはこちらにおいてありますので、コピーすれば試すことができます。
パッケージのインストールとと証明書の作成、DBの作成を行うことで動かすことができます。

動かすまでの手順は以下になります。

  • Githubからのソースダウンロード
work # git clone https://github.com/tom-m-m/ibmi-nodejs-qr-detection.git
Cloning into 'ibmi-nodejs-qr-detection'...
remote: Enumerating objects: 40, done.
remote: Counting objects: 100% (40/40), done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 40 (delta 15), reused 35 (delta 10), pack-reused 0
Receiving objects: 100% (40/40), 3.93 MiB | 3.15 MiB/s, done.
Resolving deltas: 100% (15/15), done.
  • パッケージのインストール
    Githubにはモジュールをアップロードしていないため、インストールが必要となっています。
    package.json があると、npm install で必要なパッケージがインストールできます。
work # cd ibmi-nodejs-qr-detection/
ibmi-nodejs-qr-detection # ls
app.js  core  odbc.ini  package-lock.json  package.json  views
ibmi-nodejs-qr-detection # npm install

added 216 packages, and audited 217 packages in 9s

24 packages are looking for funding
  run `npm fund` for details

8 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
  • 証明書の作成
    証明書は、この記事の上部で記載している内容と同様になります。
ibmi-nodejs-qr-detection # mkdir key
ibmi-nodejs-qr-detection # cd key/
ibmi-nodejs-qr-detection/key # openssl genrsa -out server_key.pem 2048
ibmi-nodejs-qr-detection/key # openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Tokyo/L=Musashino-shi/O=Foo/OU=Bar/CN=foo.bar.com"
ibmi-nodejs-qr-detection/key # openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Certificate request self-signature ok
subject=C = JP, ST = Tokyo, L = Musashino-shi, O = Foo, OU = Bar, CN = foo.bar.com
ibmi-nodejs-qr-detection/key # cd ..
ibmi-nodejs-qr-detection # node app.js 
Listening on port 9443!

あとは、DBの作成を行うと完了です。
IDを含むQRコードは、Webのサービスを使うと簡単に作成できます。
Node.js にもQRコード作成用のパッケージもあります。

4編に渡る記事になりましたが、このシリーズは終了となります。
ありがとうございました。

3
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
3
0