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

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

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

rand.Seed()はdeprecated(Go lang)

VScodeでGo 1.20以降の環境では、rand.Seed()は使うなというwarningが出ます。

代わりに推奨されているのは、rand.NewSource()になります。

	rand.Seed(time.Now().UnixNano())
	rand.NewSource(time.Now().UnixNano())

理由は以下のポップアップメッセージですが、a dependency changes how much it consumes from …..の意味は理解できません。最後の一行を読むと、他のパッケージからもglobal random resourceにアクセスがあると期待したシーケンスで出力されなくなると読めるのでリソース(global random resource)を共有時の問題のようで、他のパッケージからアクセスできないrand.NewSource()を使えということなのでしょう。

stackoverflowのQAを見るとこの件がアップされていますね。

https://stackoverflow.com/questions/75597325/rand-seedseed-is-deprecated-how-to-use-newrandnewseed

 

admin

 

struct{}とstruct{}{}(Golang)

struct{}は型(type)を表現しているし、struct{}{}はstruct{}のインスタンス、従って変数の宣言時には型表現としてのstruct{}を使い、変数への代入時には値(インスタンス)としてのstruct{}{}を使わないといけません。

var empt struct{} 
empv := struct{}{}

考えてみればそうですが、混乱しやすいかもしれない

 

admin

doneパターン(Golang)

Goでの特徴的な機能の一つであるgoルーチンですが、その終了判定あるいはチャネルの選択時の一つのパターンがdoneパターンと言われるもの。

コードのベースは以下からですが、

https://github.com/mushahiroyuki/lgo/blob/main/example/ch10/ex1009.go

実はチャネルサイズが10なので、以下のgoルーチンのループでは11以降はチャネルへの書き込みができないので待ちになります。実質はmainルーチンが終了するとgoルーチンも終了されるので処理上の問題はないのですが、

ここでgoルーチン側で終了待ちをするためのdoneパターンを導入してみます。

close(done)がgoルーチンへの終了シグナルになりますが、goルーチン側ではselect/caseを使っています。チャネルの処理が継続できる条件が複数あるときに、継続可能となった処理をselect/caseで選択して実行するものです。select/case文の特徴的なところは、複数の条件が成立しているときにはランダムで継続可能処理を選ぶことでswitch文のように上から順番のような優先順位はつけないということです。

func main() {
	ch := make(chan int)
	var result []int

	done := make(chan struct{})

	go func() {  // 処理してもらう数値をchに入れる
		for i := 0; i < 100; i++ {
			select {
			case <- done:
				fmt.Print(". ")
				return
			case ch <- i:
			}
		}
	}()
	
	result = processChannel(ch)
	close(done)
	
	time.Sleep(1000)
	fmt.Printf("result: %d\n", result)
}

このサンプルでは確実にdone処理を実行するためにmainルーチンで1000nsの待ち時間を入れています。

実行すると、例えば以下のようになります。

ゴルーチン 起動完了
process: 0 0 0
process: 9 81 0
process: 5 25 2
process: 6 36 2
process: 4 16 0
process: 7 49 1
process: 1 1 2
process: 8 64 2
process: 2 4 1
process: 3 9 1
. result: [0 81 16 9 4 49 64 25 1 36]

 

admin

 

Stringer interface(Golang)

今更ですが、fmtパッケージにはStringerインターフェースが定義されているので、StringerインターフェースのメソッドString()を独自に実装すると、実装したメソッドが有効となるのでその書式でPrintされます。

以下はGlangのAPIからのコードですが、String()を実装すればその書式になるし、未実装ならば構造体のままでプリントされています。

package main

import (
	"fmt"
)

// Animal has a Name and an Age to represent an animal.
type Animal struct {
	Name string
	Age  uint
}

// String makes Animal satisfy the Stringer interface.
func (a Animal) String() string {
	return fmt.Sprintf("%v (%d)", a.Name, a.Age)
}

func main() {
	a := Animal{
		Name: "Gopher",
		Age:  2,
	}
	fmt.Println(a)
}
<String() string実装>
Gopher (2)

<デフォルト>
{Gopher 2}

多少事情は異なりますがtime.Timeでは、time.Timeが独自のString()メソッドを実装しているから意図した通りにはならず独自のStringメソッドが補完してくれるようです。

https://tutuz-tech.hatenablog.com/entry/2019/11/28/091036

 

admin

 

関数に可変長引数をスライスで渡すとき(Golang)

Goで可変引数を渡すときにの記述方法についてです。

可変長引数は関数で内部的にはスライスに変換されるのでスライスで渡すこともできますが、関数側は”v1(names ...string)“のように受け取りますが、呼び出し側は”v1(names...)“のように…が関数とは逆の位置に来ますよということです。

/*
Variadic functions in Golang
*/ package main import ( "fmt" ) func v1(names ...string) { // pack operator used before type in argument fmt.Println(names) } func main() { names := []string{"Albert", "Issac"} v1("John", "Jane", "Dexter", "Bruce") // [John Jane Dexter Bruce] // Here is unpack operator in action v1(names...) // [Albert Issac] name1 := []string{"John", "F.", "Kennedy"} v1(name1...) }

決まり事ではありますが、論理的に考えれば受け取り側は型を指定していて、呼び出し側は変数名(スライス名)を指定しているから合理的とは言えます。

 

admin