Wi-Fi無しのラズパイにWi-Fi機能追加

元々Linux用のUSBドングルでまともなものは無いのは知っているから、Wi-Fi機能のない初代に近いモデルにWi-Fi機能を追加するのはおそらくこれが一番手っ取り早い。

TP-LINKの携帯用のWi-Fiルーターを天板に両面テープで貼り付けただけ。元々このモデルはルーター/中継機/クライアントの三つのモードがあるので、クライアントモードを設定すれば良い。

 

電源にtype-b2本のUSB必要だけど、見た目一体感はありそう、ちょいダサいけど。

 

admin

Dockerコンテナからイメージを作成する

誤ってコンテナ削除すると、それまでに適用したアップデートが全て消えるので、コンテナからイメージを作成する方法。

コンテナを停止、docker psで見えない状態、してからたとえば以下のようなコマンドを実行すればrasp32_image_goという名前のイメージが作成されます。作成後はrunでコンテナ(ここではrasp32_go)を作成して内容を確認しておけば大丈夫です。

% docker commit rasp32 rasp32_image_go

<image>

<container>

rasp32コンテナに適用した変更を保存するために、rasp32_image_goというイメージを作成しています。簡単ですね、

必要ならDockerhubにアップロードしておけば共有やバックアップができます。個人で使う分にはDockerhubはバックアップでしかないですが。

共同作業をする対象ならば、historyが見えなくなるので使いづらいでしょうが、個人で使う分にはDcokerfileを使わなくてもこれで十分だろうと思う。

 

admin

DockerでラズパイのGolang build環境を作る

https://isehara-3lv.sakura.ne.jp/blog/2023/04/19/golangアプリは単純にクロスビルドしても動かない(db/

の対応としてとりあえずラズパイ自身でbuildさせましたが、ラズパイModel B+でbuildに必要な時間は、実行ファイルのタイムスタンプからおよそ二時間。これでは実用的ではないので代替え方法を考えるけれども、一番楽そうなのはDockerを使うことでしょう。

自分の環境を汚染させないとか、イメージの配布とかではなく、ラズパイの環境を作るための使用(DockerはM1 Macにインストール、ターゲットのコンテナはraspbian32 OS)です。

多少以前の記事になりますが、以下を参考に実行。raspbianイメージとかは最新版、と言っても2020年度が最新ですが。

https://www.koatech.info/blog/raspbian-on-docker/

・イメージ取得と作成

% wget http://ftp.jaist.ac.jp/pub/raspberrypi/raspios_lite_armhf/root.tar.xz

% docker image import ./root.tar.xz raspbian-stretch-lite:2020

・以下のコマンドでwarningが出る、とりあえずは無視して大丈夫な様子(QEMUは元々Intel CPU用だからか、Apple siliconでも今のところ何とかなってるけど)

% docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested

exec /register: exec format error

・コンテナ作成と起動(名前指定)

% docker run -it --name rasp32 raspbian-stretch-lite:2020 bash

・作成しているコンテナを起動するなら

% docker exec -it rasp32 bash

・Golang install

# wget https://golang.org/dl/go1.20.1.linux-armv6l.tar.gz

# sudo tar -C /usr/local -xzf go1.20.1.linux-armv6l.tar.gz

# /usr/local/go/bin/go version (パスが通っていない状態)

・Dockerのコンテナからファイルをクライアントにコピー(例)

% docker cp 21f2c6ce6eb8:/home/pi/hello .

・ディレクトリ指定すればその中全部をコピー(クライアントからコンテナへ)

% docker cp myfare 21f2c6ce6eb8:/home/pi/

コンテナ上のMacの該当ディレクトリを丸ごとコピー(gomodなど含めて)してきてbuild、できた実行ファイルをscpでラズパイに転送して起動するとちゃんと実行できました。実行ファイルの動作確認、最初は定番のハローワールドで確認しています。

エミュレーション(QEMU)なので、速度はMacでbuildするのに比較すると遥かに遅い、それでも二分ぐらいで終わっているからやはりラズパイで実行するよりはほぼ百倍速いから実用的です。

実行ファイルのサイズが微妙に違うのはライブラリの版数違いか、main1のほうがコンテナでbuildしたもの。

コンテナ上のraspbian、

ラズパイのbuild環境としてはおそらくデフォルトだと思う。カスタマイズしたコンテナを作る時にDcokerfileを記述しておけば、再現性確保できます。

 

admin

UTMのUbuntu起動しなくなった、

M1 Macbook AirでインストールしているArm版Ubuntu、アップデートが正常に終了しなくなった後で、起動しなくなった。新たにインストールし直ししか道はなさそう。

まあUTMの限界といえばそうなのかもしれないから、VMWaraのAppli silicon native版待ちかな。個人使用無償の継続期待が前提ですが。

 

admin

/etc/rc.localからsystemdになってました

ラズパイでmyfareアプリを起動時に立ち上げしようと思ってrc.localに記述しても起動しません。実は最近のLinuxのバージョンではrc.localは単にipアドレスの表示をするだけになっていて、アプリの起動はsystemdを使えということのようです。しかしsystemdに設定したつもりでも起動するとエラーになります。実行ファイルと同じディレクトリに配置しているファイルが見つからないと言われます。

$ sudo systemctl status myfare.service
● myfare.service - myfare
     Loaded: loaded (/etc/systemd/system/myfare.service; enabled; vendor preset>
     Active: failed (Result: exit-code) since Wed 2023-04-19 11:57:00 BST; 9s a>
    Process: 1805 ExecStart=/home/pi/myfare/main (code=exited, status=1/FAILURE)
   Main PID: 1805 (code=exited, status=1/FAILURE)
        CPU: 121ms

Apr 19 11:57:00 rasp-b systemd[1]: Started myfare.
Apr 19 11:57:00 rasp-b main[1805]: open uid.json: no such file or directory

事例検索して行き着いたのが、WorkingDirectory設定。

[Unit]
Description=myfare

[Service]
WorkingDirectory=/home/pi/myfare/
Type=simple
ExecStart=/home/pi/myfare/main

[Install]
WantedBy = multi-user.target

つまりこれを指定しないと、実行ファイルからファイルを見つけられなくなります。

systemdの記述方法はネットにたくさんありますが、ここに行き着くのに一時間以上。大本のマニュアル見た方が早かったかと思いますが、ともかくも以下の一連のコマンドの手順で自動起動できました。


サービスファイルを記述して、daemonのリロード
$ sudo systemctl daemon-reload
起動確認
$ sudo systemctl start myfare.service
正常に起動していることを確認
$ sudo systemctl status myfare.service
起動時にサービスを有効化
$ sudo systemctl enable myfare.service

 

admin

 

Golangアプリは単純にクロスビルドしても動かない(DBドライバがcgo使ってる)

Macで開発したアプリをラズパイで動かそうとしましたが、そのままでは動かない。なぜならgormもdbドライバーもcgoを使っている、つまりターゲットのgccを用意してそれを指定しないといけないから。

とりあえず動かすだけなら、すごく時間はかかりますがラズパイでビルド、2時間ぐらい放置してたらビルド完了してました。

実行ファイルを起動すると、Macよりは多少レスポンスは遅いのですがちゃんと動作しています。

<layout.html>

これだけはws://mbair.local:8080/wsをラズパイに変更が必要です。

 window.onload = function () {
  socket = new WebSocket("ws://mbair.local:8080/ws");
  socket.onopen = function () {
    append_message("system", "Socket Connected");
  };
  socket.onmessage = function (event) {
    append_message("server", event.data);
  };

  const send = function (){
    socket.send("")
  }
  setInterval(send, 500);

};

クロス環境をどうするかですが、Dockerがおすすめのようなので、それでやってみます。

メモリ使用状況は、こんな感じです、クライアント一台だけで、

すぐにもう一台増やすと、およそ200KBぐらい増えていますが、この程度では普通には十分です。

 

admin

myFare cardアプリのアップデート

以下の記事からのアップデートになります。

https://isehara-3lv.sakura.ne.jp/blog/2023/04/14/golangでwebsocketの実装/

カードリーダーにタッチで参加登録してブラウザで状況が見れる、追加で参加登録があるとwebsocketでその旨のメッセージを表示(画面更新はマニュアル)、発表者は登録のリンクをクリック(トグルになっています、Wi-Fi内の限定ユーザーなのでpwとかは要求しません)すると発表登録というアプリです。

コードの構成は、

uid.jsonはuidとnameのjson形式ファイルです。

全体のコードは、

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

になりますが、websocket経由メッセージのブロードキャストのためにコードは以下のようなっています(main.goの部分抜き出し)

① 新規参加者の登録あればその旨のメッセージをserial.goからチャネル経由でmain.goに通知する

② wsはブラウザからのリクエストの都度新規に作成されるから、通知内容が消滅しないようにmain.goの中で共通エリアに保存

③ 余計なwsを消すために、ブラウザから定期的にwebsocket経由でメッセージ送ってforループ内でタイムアウト(今は3秒に設定)すれば未使用wsと判断して終了させる、clients[ws] = trueで確認できるようにしてます

④ ②で保存された通知内容は各ws内で未送信(前回送付と同じかどうか判定、つまり同じ内容は2回以上送らない)であれば送信するが送信条件はタイマーで設定、チャネルから受信と並列条件としています

スマートではなさそうだからもっと別の方法がありそうです。

func msgHandler(ws *websocket.Conn) {
	defer ws.Close()

	clients[ws] = true
	fmt.Println(clients)

	premsg := msg	// initialize websocket message
	t := time.NewTicker(500 * time.Millisecond)
	defer 	t.Stop()
label:
	for {
		msgr := ""
        err := websocket.Message.Receive(ws, &msgr)
		if err != nil {
			//log.Println("receive error")		// main pupose is to check timeout (to detect unused session)
			break label
		}

		select {
			// to send websocket message triggered by the timer
			// the reason to separate receive and send is ws are running multi thread
		case <- t.C:
			if premsg != msg {
				premsg = msg
				err := websocket.Message.Send(ws, msg)
				if err != nil {
					log.Println("send err")
					break label
				}
			}
		case name := <- uidSerial.Notice:	// wait for message from serial.go via channel
			mu.Lock()
			msg = name.(string)
			mu.Unlock()
		}
	}
	clients[ws] = false
}

 

admin

SQLite3で指定できるデータ型

Myfare cardを追加してテーブル作成しようとしたら、特定のカードのテーブルの値がstring指定したはずなのに”Inf”になってしまった。Infとはおそらく無限大ということだろうからstring型を指定しても機能しないらしい。

で調べるとSQLiteで指定できる型にはstringは無い。

https://www.javadrive.jp/sqlite/type/index1.html

おそらくstringを指定しても整数扱いとなってオーバーフローしてしまったらしい。

string -> text

int -> integer

に変更して作り直してうまくいっているようです。GORMで指定する構造体ではこのように変更しようがない(おそらくGORMが自動で変換する)から、テーブル作成時のSQLコマンドだけでの対応。

 

admin

Golangでwebsocketの実装

websocket自体はRFCで規定されているものですが、実装はそれぞれの言語ごとに存在します。

サンプルは、

https://zenn.dev/empenguin/articles/bcf95c19451020

元を辿ると、おそらくこちら。

https://echo.labstack.com/cookbook/websocket/

他の言語の例に漏れず、クライアント側のJavaScriptとサーバー側のGoのスクリプトが連携して動作します。ディレクトリ構成は以下の通りです。

<main.goのリクエスト処理部分>

func handleWebSocket(c echo.Context) error {
	websocket.Handler(func(ws *websocket.Conn) {
		defer ws.Close()

		// 初回のメッセージを送信
		err := websocket.Message.Send(ws, "Server: Hello, Client!")
		if err != nil {
			c.Logger().Error(err)
		}

		for {
			// Client からのメッセージを読み込む
			msg := ""
			err = websocket.Message.Receive(ws, &msg)
			if err != nil {
				//c.Logger().Error(err)
			}

			// Client からのメッセージを元に返すメッセージを作成し送信する
			err := websocket.Message.Send(ws, fmt.Sprintf("Server: \"%s\" received!", msg))
			if err != nil {
				//c.Logger().Error(err)
			}
		}
	}).ServeHTTP(c.Response(), c.Request())
	return nil
}

最初のページリクエスト後はwebsocketのループに入るので、ここでページのリロード(複数回ルートのリクエスト)をするとループ処理(for loop)でエラーが発生します。

一点疑問はwebsocketのポート番号指定をどこでやっているのかわからないこと。

P.S. 2023/4/15

ポートはHTTPと同じポートが使われています。WSはHTTPとは異なるプロトコルなので同じポートでも問題はないようですね。

 

admin

Golangのテンプレートエンジン(html/template)

動的なページ作成のためにはテンプレートエンジンが必須で、Node.jsの場合にはexpress-generatorに同様の機能が存在しますが、golangでもライブラリhtml/templateを使うと実現できます。

https://isehara-3lv.sakura.ne.jp/blog/2023/03/29/golangとnode-jsでmifareカード使ってチェックイン表示/

ではビューはNode.jsを使っていますが、ラズパイで動かすと実行速度あるいは実行ファイルの扱いやすさなどを考えるとGolangに統一した方が良いだろうからhtml/templateを使います。myfareカードから読み取りデータを受信するGoのスクリプトはほぼそのままです。

https://www.twihike.dev/docs/golang-web/templates

上記を参考に作成してみました。

<web server起動のスクリプト>

myfareカードを読み取ってDBをアップデートする関数はgoオプションで並行処理として起動(go uidSerial.SerialMain())。

package main

import (
	"fmt"

	"html/template"
	"myfare/uidSerial"
	"net/http"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"strconv"
	"time"
)

type Ninjya struct {
	Uid  string `gorm:"primaryKey"`
	Name string
	Time int
	Stat int
}

var products []Ninjya
var ninjyaSlice []string

// implements TableName of the Tabler interface
func (Ninjya) TableName() string {
	t := time.Now()
	tableName := "tbl" + strconv.Itoa(t.Year()) + strconv.Itoa(t.YearDay())
	return tableName
}

// to get active ninjya slice
func ninjya() {
	db, err := gorm.Open(sqlite.Open("./myfare.db"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	// Migrate the schema
	db.AutoMigrate(&Ninjya{}) // if you use Automigrate and change struct, it won't be reflected automatically

	db.Debug().Order("Time desc").Where("Stat = ?", 1).Find(&products) // SELECT * FROM where Stat = tbl*****;
	ninjyaSlice = nil
	for i, p := range products {
		ninjyaSlice = append(ninjyaSlice, p.Name)
		fmt.Println(i, p)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	t := template.Must(template.ParseFiles("layout.html", "pageData.html"))
	// to update ninjya status
	ninjya()
	tm := time.Now().Format(time.RFC1123)
	err := t.Execute(w, map[string]interface{}{
		"Time": tm,
		"Slice": ninjyaSlice,
	})
	if err != nil {
		fmt.Fprintln(w, err)
	}
}

func wevServer() {
	mux := http.NewServeMux()
	// to include static resoureces
	mux.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
	mux.HandleFunc("/", handler)
	server := http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	err := server.ListenAndServe()
	if err != nil {
		if err != http.ErrServerClosed {
			panic(err)
		}
	}
}

func main() {
	// to call card reader function()
	go uidSerial.SerialMain()

	// http server start
	wevServer()

}

t := template.Must(template.ParseFiles("layout.html", "pageData.html"))

ここで、以下の二個のtemplete処理対象ファイルを読み込みます。

<layout.html>

<pageData.html>

mux.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))

スタイルシートを読み込むために検索パスを追加します。

 

実行すると、例えばこんな具合にブラウザ上に表示されます。

これでNode.jsと同等レベルになったので、Golang上で機能を追加していきます。

 

admin