GolangとNode.jsでmifareカード使ってチェックイン表示

Mifareカードリーダーを使ったチェックインシステムを
GolangとNode.js使って雛形もどきを作ってみた。Node.jsはwebアプリ雛形作成のexpress-generatorを使っています。

GolangでSQLite3のデータベーステーブル作成とアップデート、Node.jsはwebサーバー起動してviewを作成しています。

・index.js:データベースのテーブルから読み出したチェックイン情報を読み出して、ページ(view)の必要情報を用意します。データベース読み込みは非同期処理なので、async/awaitで同期化します。

・index.pug:index.html作成のテンプレートです。pugファイルにはJavaScriptの記述もできて繰り返し処理が簡単にできるから便利です。

・style.css:スタイルシート、本来のディレクトリから一個上の階層に移動してあります。GitHubへの転送がそうしないとうまくできなかったから。

・serial.go:M5StackC plusからのカード読み取り情報を受け取りjsonのuidと名前対応情報から手ブルを作成。テーブル名は年号と1月1日からの経過日を組み合わせたものにしています。

コードは分散していますが以下のリンクから、

https://github.com/chateight/golang/tree/master/serial

P.S. 2023/4/1

Unmarshalは簡単に使えてこのケースなら別に問題もないですが、巨大なファイルになるとメモリ大量に消費するだろうから効率考えたらストリームとして処理するのが良いようです。

 

*_tool.jsはデバッグ用のコードです。

カードリーダーのM5StackC plusのコードなどの情報は、

https://github.com/chateight/rfid

 

admin

SQLite3の非同期処理を同期化させる(Node.js)

便利なようで結構不便なJavaScriptのシングルタスクを有効に使おうとするイベント処理。非同期処理は簡単な使い方では不便でSQLite3のDBアクセスライブラリには同期型も存在するらしい。

ユーザーが多いだろうから非同期型のライブラリでなんとかしようとするとPromise処理が必要になります。最新の機能ではawait/asyncになりますが。以下はexpress-generatorで発生されたwebページのディスパッチ処理を行うindex.jsにデータベースアクセス処理を同期させて追加したコードです。

<index.js>

var express = require('express');
var router = express.Router();

let nameArray = new Array();

/* GET home page. */
router.get('/', function(req, res, next) {
  var response = Math.floor(Math.random() * (100 - 1)) + 1;

  let t1 = new Date();

async function dbReadExec(){
  try{
    nameArray = await dbRead();
    let t2 =new Date();
    console.log("dbReadEec",nameArray, t2 - t1);
    res.render('index', { title: 'Express', data: response, db: nameArray});
  } catch (err){
    console.log('error');
  } finally {
    console.log("done");
  }
}

dbReadExec();

});

//
// DB handler
//
function dbRead(){
  return new Promise((resolve, reject) => {

  const sqlite3 = require("sqlite3");
  const db = new sqlite3.Database("/Users/username/go/src/serial/myfare.db");
  
  // calc file name
  let d = new Date();
  let year = d.getFullYear();
  let month = d.getMonth();
  let day = d.getDate();
  
  let date1 = new Date(year, 0, 0);
  let date2 = new Date(year, month, day)
  let behind = Math.floor((date2 - date1) / (24*3600*1000));      // convert milisec to day
  
  nameArray = [];       // clear Array
  
  db.serialize(() => {
  db.each("select * from tbl" + year + behind + " where stat=0", (err, row) => {
    if (err) {
      reject(err);
    }
  //console.log(`${row.id} ${row.name} ${row.time} ${row.stat}`);    
  nameArray.push(row.name);
  resolve(nameArray);
  })
  
  db.close();

  });
  });
}


module.exports = router;

serialize()したつもりでも、resolve(nameArray);をdb.close()の後にするとうまく動きません。ここも非同期で動くから?ループ内でresolve()を複数回発行することになりますが、これはthenがchainされて最後のresolve()が有効になるようです。データ量が少ないなら、db.each()ではなくdb.all()で一回で返せば良さそうに思いますが。

let t2 =new Date();

console.log("dbReadEec",nameArray, t2 - t1);

はデータベース処理時間計測のためのタイムスタンプです。二回目以降はJavaScriptのキャッシュが有効だろうから高速です。

シングルタスク言語というのはちょっと複雑な構造のコードを書こうとすると不便なところが目立つように感じます。

 

P.S. 2023/3/28

db.each部分だけをdb.allに置き換え、

  db.all("select * from tbl" + year + behind + " where stat=0", (err, rows) =>{
    rows.forEach(row => nameArray.push(row.name));
    //console.log(nameArray);
    resolve(nameArray);
})

こちらの方がスマートでしょう。

 

admin

 

Node.jsフレームワーク(Express-Generator)でのページ遷移

ラズパイにはExpress-Generatorをインストールしていましたが、MacBookにもインストールして久しぶりに触っています。Express-Generatorはwebアプリ雛形作成のためのツールの一つです。webアプリ作成のためには理解すべき点がいくつかありますが、動的なページを作成するためのデータの与え方はその一つです。

以下は関連するディレクトリを表示していますが、index.jsでviews配下のindex.pugのコンテンツを作るようになっています。

index.jsソースの一部(dbName())は記載していませんが、データベースからの読み出しを行なっています。本タイトルと直接の関係はないので省略。

var express = require('express');
var router = express.Router();

var nameList = "";

/* GET home page. */
router.get('/', function(req, res, next) {
  var response = Math.floor(Math.random() * (100 - 1)) + 1;
  
  dbName();

  const wait = () => {
  res.render('index', { title: 'Express', data: response, db: nameList});
  }
  setTimeout(wait, 3);
});

res.render()の第二引数以下がindex.pugに渡されてhtmlのコンテンツを構成しています。雛形ではtitle一個しかありませんが、カンマ区切りで任意の個数を追加できます。ここでは1~100までの範囲の乱数をdataとして、データベースから読み出したデータを変数名dbでindex.pugに渡しています。

index.pug

extends layout

block content
  h1= title
  p Welcome to #{title}

  p #{data} 

  p #{db}

SQLite3のデータベースアクセスも非同期で実行されるので、ページ作成が先行して実行されてdbデータの供給が間に合わないので、仮に時間待ち3msを入れていますが、あくまでデバッグ専用でいずれ修正の予定。

 

admin

 

 

Node.js(JavaScript)で経過日(時間)を求める

GolangにはtimeパッケージにYearDay()という年初からの連続で今日が何日目かを返してくれる関数があってそれをファイル名(tbl + 西暦年 + 連続何日目)に使っています。1日に2個以上テーブル作る可能性がないから、これで一意にファイル名を決定できます。

https://pkg.go.dev/time

でもNode.jsで同じファイル名を指定しようとするとこのような関数はないので、計算が必要になります。なぜNode.jsで必要かというとGolangで作成したSQLiteのテーブルにNode.jsからアクセスしたいからですが。

Golangでのテーブル作成、年号と年初からの経過日でファイル名を作成しています。以下のコードの一番下の行でターブル作成。

date := strconv.Itoa(t.Year()) + strconv.Itoa(t.YearDay())

	if del {
		cmd := "drop table if exists tbl" + date

		_, err := db.Exec(cmd)
		if err != nil {
			fmt.Println("db table drop error")
		}
	}

	// table exist check
	cmd := "select*from tbl" + date
	_, table_check := db.Query(cmd) // checked by using return code

	if table_check != nil {

		cmd = "create table if not exists tbl" + date + "(id string primary key, name, time int, stat int)"

JavaScriptでのテーブル名の作成スクリプト

// calc file name
let d = new Date();
let year = d.getFullYear();
let month = d.getMonth();
let day = d.getDate();

let date1 = new Date(year, 0, 0);
let date2 = new Date(year, month, day)
let behind = Math.floor((date2 - date1) / (24*3600*1000));      // convert milisec to day


db.serialize(() => {
    db.each("select * from tbl" + year + behind + " where stat=0", (err, row) => {
    console.log(`${row.id} ${row.name} ${row.time} ${row.stat}`);
    })
});

Math.fllor():端数切り捨てはなくても大丈夫のようですが、入れておいて実害はないだろうから。

 

admin

Node.jsでSQLiteにアクセス

Golangで作成したSQLiteのテーブルをnode.jsからアクセスしてみようと思ったが、前提になるnpmが起動できないし、nodeも起動できない。

% node   

dyld[71646]: Library not loaded: /opt/homebrew/opt/icu4c/lib/libicui18n.71.dylib

  Referenced from: <66A3E1EC-93E2-36D1-889D-02EE5C192FCC> /opt/homebrew/Cellar/node/18.11.0/bin/node

Reason: tried: ‘/opt/homebrew/opt/icu4c~~~~以下省略

解決方法は、

https://stackoverflow.com/questions/53828891/dyld-library-not-loaded-usr-local-opt-icu4c-lib-libicui18n-62-dylib-error-run

から、

% brew reinstall icu4c

で解決。

・Node.jsからSQLiteへアクセスするドライバーをインストール

% npm install sqlite3

・すでに存在しているテーブルにアクセスしてみる、

//

// DB handler

//

'use strict';

const sqlite3 = require("sqlite3");

const db = new sqlite3.Database("./myfare.db");

db.serialize(() => {

    db.each("select * from tbl202382 where stat=0", (err, row) => {

    console.log(`${row.id} ${row.name}`);

    })

});

db.close();

do.run()が非同期実行なのでsierialize()が必要とのことですが、データ更新してなくて読んでるだけで並行処理されるものがないから実はserialize()はこのケースでは不要。

しかし実行すると、

Error: Cannot find module ‘sqlite3’

対策はパスを通すことらしいので、

% export NODE_PATH=`npm root -g`

で、

% echo $NODE_PATH

/opt/homebrew/lib/node_modules

にsqlite3は存在していますが、

% which sqlite3

とは違うディレクトリなので実はシンボリックリンクを使っているのかもしれない。

ともかくもSQLite3のテーブルアクセスはできた。

SQLiteでテーブル存在有無を確認(Golang)

GolangでDBドライバー使ってコードからテーブルの存在有無を確認する方法です。SQLコマンド打てば簡単ですが、コードでチェックするというのはどうやるかしばらく悩みました。以下のstackoverflowから引用しました。

https://stackoverflow.com/questions/56915022/check-if-database-table-exists-using-golang

動作環境でimportしているライブラリ(DBアクセスで必要なのはsqlとsqlite3の2個)、go-sqlite3は使わないけどインポートだけはしないとコンパイルできません。

go-sqlite3のブランクインポートが必要な理由は、

https://maku77.github.io/p/kgzfwdt/

になります。

import (
	"bufio"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	_ "github.com/mattn/go-sqlite3"
	"log"
	"os"
	"strconv"
	"time"

	"go.bug.st/serial"
	"go.bug.st/serial/enumerator"
)

以下がテーブル存在をチェックしているコードになりますが、dateはフィアル名の一部ですね。戻りのerrorコードがnilならテーブル存在、それ以外ならテーブルが存在しないことになります。

	// table exist check
	cmd := "select*from tbl" + date
	_, table_check := db.Query(cmd)			// checked by using return code

	if table_check != nil {

方法は別にSQLite限定ではなくて他のDBでも同じだろうと思います。

 

admin

ioutilもdeprecated(Golang)

タイトルの通りですが、1.20以降ではそうなります。実は1.16でそういう風に宣言されていたらしい。以下のリンクを参照、

bytes, err := ioutil.ReadFile("sample.json")

bytes, err := os.ReadFile("sample.json")

おそらくファイル全体を読み込むのは、メモリ効率が良くないし、Goの美学とも相入れないのでos.ReadFileを使えということなんだろうと思います。

https://future-architect.github.io/articles/20210210/

 

admin

M5StackC plusとGolangでシリアル通信する

M5StackC plusからのMyfareカードのUID情報をUSBシリアル使ってGoで受信してみます。

参考は、

https://zenn.dev/nnabeyang/articles/d54f18cc39dc4a654c7a

USBシリアルのVID/PIDをMacBook Airからシステム情報/USBで引くと、以下のように見える。USB経由でファイル(USBストレージ)とシリアルインターフェースが見えるようになっています。

0x403と0x6001を使うが、これでは該当するポートが見つからないと言われる。

VID/PIDを調べるために取得スクリプトを、

https://pkg.go.dev/go.bug.st/serial/enumerator

のexampleから引っ張ってくる。参考までにPortDetails構造体の中身見ると、型が数値ではなく文字列だから16進表記じゃだめです。まあ、元々が文字列じゃないか、

if port.IsUSB {
fmt.Printf("   USB ID     %s:%s\n", port.VID, port.PID)
fmt.Printf("   USB serial %s\n", port.SerialNumber)


-->
   USB ID     0403:6001
   USB serial AD5232ED16
type PortDetails struct {
	Name         string
	IsUSB        bool
	VID          string
	PID          string
	SerialNumber string

	// Product is an OS-dependent string that describes the serial port, it may
	// be not always available and it may be different across OS.
	Product string
}

VID/PIDの文字列をそのまま使って、

package main

import (
	"bufio"
	"errors"
	"fmt"
	"log"
	"os"

	"go.bug.st/serial"
	"go.bug.st/serial/enumerator"
)

func getPortName() (string, error) {
	ports, error := enumerator.GetDetailedPortsList()
	if error != nil {
		return "", error
	}
	for _, port := range ports {
		/*
			if port.IsUSB {
				fmt.Printf("   USB ID     %s:%s\n", port.VID, port.PID)
				fmt.Printf("   USB serial %s\n", port.SerialNumber)
		*/

		if port.IsUSB && port.VID == "0403" && port.PID == "6001" {
			return port.Name, nil
		}
	}
	return "", errors.New("M5Stack cplus is not conntected")
}

func main() {
	portName, err := getPortName()
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	mode := &serial.Mode{
		BaudRate: 115200,
	}
	port, err := serial.Open(portName, mode)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	scanner := bufio.NewScanner(port)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
}

これでちゃんと読めた。

 

admin

M1 MacBook AirのUbuntu版数

Ubuntuの18.04系が5月にサポート終了するというニュース見て、今の版数を改めて確認。

Intel MacBookは20.04系(VMWare)で、

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 22.04.2 LTS
Release:	22.04
Codename:	jammy

M1 Mac(UTM)に入れたのは最新版の22.04、まあインストール時期からすれば当然ですが。

 

admin

 

MacBook Airのゴミ箱がワンクリックで消せない

まあMacOSではありがちな現象ではありますが、ゴミ箱がワンクリックで消せなくて、わざわざゴミ箱の中に入ってファイルを全選択して消去。

週一のリフレッシュブートで回復しているから、何らかのOS問題。回避が簡単な問題なら最近気にならなくなった。

 

admin