HUB75 LEDアレイ表示にテキストのイメージ化機能も追加

https://isehara-3lv.sakura.ne.jp/blog/2025/01/23/hub75-ledアレイの表示画像を任意に変更できるようにし/

作成されてるイメージのアップロードだけではなく、テキスト入力してそれをイメージ化して送信するように機能追加、フォントはMacでは、

fontPath:=“/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc”
を使っていますが、ラズパイではヘッドレスでフォントもインストールされてないからそこから始めたないとダメ

コードのGitHubへのリンクに変更はありません

<ブラウザ画面>

<表示されたイメージ>

LEDアレイでの表示ポジションや縮尺、倍率などの改良の余地はありますが、機能的にはこれでほぼ完成形

 

admin

 

 

 

HUB75 LEDアレイの表示画像を任意に変更できるようにした

固定した画像ファイルを選択するだけではイマイチだから、webサーバー(ラズパイzero)にブラウザ経由で任意の画像をアップロードして、サーバー側で画像の圧縮処理、rgb抽出を行いTCPでラズピコに送信して表示させるようにした

構成はwebアプリをgolangで作成し、ラズピコ側のコードも対応して変更、途中でデータロスはできないからUDPはあり得なくてTCPは前提

コード生成はPerplexityが無償でもgolangでは使えるコードが出てくるので活用、多分コードの生産性からいったら倍以上にはなる

<web application code>

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

<raspberry pi pico code>

https://github.com/chateight/hub75_led_array_image_upload/tree/main

ラズパイzeroでの実行速度は150KBぐらいのイメージファイルでLEDアレイに表示されるまで2秒ちょっとといったところ(Wi-Fi転送は過去の経験で1MB/secは出るからほぼイメージ処理の時間)だけど実用的には問題ないレベル

ラズピコ側でTCP通信するのにどうやるのかを検索してもイマイチだったので、それもPerplexityからの回答で、主要なコードを抜き出すとこんな感じ、

void setup(){
  IPAddress staticIP(192, 168, 1, 200); 
  IPAddress gateway(192, 168, 1, 1);
  IPAddress subnet(255, 255, 255, 0);

  WiFi.config(staticIP, gateway, subnet);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.println("Connecting to WiFi...");
  }
  server.begin();
}

void loop() {
  client = server.accept(); // TCP start
  // if network is active
  if (client) {
    while (client.connected()) {
      if (client.available()) {
        // receive binary data
        int bytesRead = 0;
        unsigned long startTime = millis();
        
        // Read all available data within a 500ms window
        while (client.available() && (millis() - startTime < 500) && bytesRead < MAX_BUFFER) { 
      receivedData[bytesRead] = client.read(); 
      bytesRead++; 
     }
        
        // response to the server
        const char* response = "Resp from Pico W!";
        client.write(response, strlen(response));
      }
  }

今更ながらコード作成でもLLMはもはや必需品で、用途ごとに要求されるものが違うからそれは人間社会と同じかもしれない

 

admin

 

 

golang/Rustのラズパイ/ラズピコ向けのクロス環境について

ラズパイ/ラズピコ限定の話になりますが、現時点での整理です

① golang : ラズパイ用にはcgoがあるとターゲットマシン用にgolangとc用に二種類のコンパイラが必要になるので、単純化のためにはcgo freeのライブラリだけ使って、(現時点ではcgoライブラリはほぼ回避手段があるはず)クロスコンパイルを実行する

たとえば、ラズパイzero用バイナリならば、以下のようなコンパイルオプションつけて実行できる

% GOOS=linux GOARCH=arm GOARM=6 go build -o rasp_zero

ラズピコで組み込み用にはgolangというのは選択肢にはならないしそもそも手段があるのか、golang使うならRustでしょう

② Rust:ラズパイ用はCross + Dockerが一番素直か、Intel MacならばVM上のUbuntuでもtoolchainが存在してるからできるけど

https://www.freecodecamp.org/news/embedded-rust-programming-on-raspberry-pi-zero-w/#heading-how-to-set-up-cross-compilation

ラズピコは組み込みだからクロスビルドしかなくて、Baker link. ENV + Rancher Desktop(docker)がCross + Dockerに相当する機能で使える、ラズピコはデバッガ(debug probe)を簡単に使えるという点でもBaker link. ENVが良さそう、おそらくBaker link. ENVと類似のツールはこの先出てくるかもしれないけど、いずれにしてもそれは仮想環境を使ったもので開発環境を自分で作ることが必要なケースはそれほどないはず(Docker用のイメージ作成は別にして)、いまだラズピコでRustはかなりニッチではあるから歩みは遅いかもしれないが

 

admin

golangにおけるcgo freeドライバに関連して

二年ぐらい前には、まだcgoしか無いようなドライバもそこそこ存在してましたが、cgo freeのドライバに変えてコンパイルをすっきりさせた方が良さそうです

cgo freeでは無いドライバが含まれるコードを普通にコンパイルしてラズパイzeroで実行させると、

$ ./rasp_zero
2025/01/12 17:17:17 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": 
	create table foo (id integer not null primary key, name text);
	delete from foo;

と言われるし、cgo対応のlinkerはいまいち不明だし、というわけで以下のリンクのサンプルコード(実は上のエラーメッセージはオリジナルのソースをコンパイルしてます)をcgo freeドライバに入れ替え

https://zenn.dev/teasy/articles/go-sqlite3-sample

github.com/mattn/go-sqlite3

を、

modernc.org/sqlite

に変更して、db openのコードを変更するだけでcgo freeとなって、

% GOOS=linux GOARCH=arm GOARM=6 go build -o rasp_zero

でコンパイルするとラズパイzeroで動くバイナリが出力されます

実はcgo free以外に、M1 Macの動作ではgo run ***では動作しても、./***では起動も終了もしないという不思議な現象が出てましたが、この件も一緒に解決、ただしこの現象ではIntel Macでは出ないという怪しげな現象でした

orm(例えばgormなど)もcgo freeにできるので、今後はcgoが必要なのはおそらくよほどな時になりそうです

<コードは>

sql.open()が変わっただけです

package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	_ "modernc.org/sqlite"
)

func main() {
	os.Remove("./foo.db")

	// original
	// db, err := sql.Open("sqlite3", "./foo.db")
	//
	db, err := sql.Open("sqlite", "./foo.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	sqlStmt := `
	create table foo (id integer not null primary key, name text);
	delete from foo;
	`
	_, err = db.Exec(sqlStmt)
	if err != nil {
		log.Printf("%q: %s\n", err, sqlStmt)
		return
	}

	tx, err := db.Begin()
	if err != nil {
		log.Fatal(err)
	}
	stmt, err := tx.Prepare("insert into foo(id, name) values(?, ?)")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()
	for i := 0; i < 100; i++ {
		_, err = stmt.Exec(i, fmt.Sprintf("こんにちわ世界%03d", i))
		if err != nil {
			log.Fatal(err)
		}
	}
	tx.Commit()

	rows, err := db.Query("select id, name from foo")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var id int
		var name string
		err = rows.Scan(&id, &name)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name)
	}
	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}

	stmt, err = db.Prepare("select name from foo where id = ?")
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()
	var name string
	err = stmt.QueryRow("3").Scan(&name)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(name)

	_, err = db.Exec("delete from foo")
	if err != nil {
		log.Fatal(err)
	}

	_, err = db.Exec("insert into foo(id, name) values(1, 'foo'), (2, 'bar'), (3, 'baz')")
	if err != nil {
		log.Fatal(err)
	}

	rows, err = db.Query("select id, name from foo")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var id int
		var name string
		err = rows.Scan(&id, &name)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name)
	}
	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}
}

P.S.  2025/1/13

”実はcgo free以外に、M1 Macの動作ではgo run ***では動作しても、./***では起動も終了もしないという不思議な現象が出てました”

この件に関連して、DB(sqlite3)を使う別のコードではビルドもできず、やはりIntel Macでは問題なくビルドできて動作したので、おそらくVenturaのアップデートと関連ありますね

 

admin

Golangのモジュールのアップデート

Golangを 1.21にアップしたら、buildでgo.bug.st/serial関連のエラー出るので、関連部分のソースコードは触っていないのでモジュールを最新にアップデートした

% go list -m all
~~
go.bug.st/serial v1.6.2
~~

前後省略

これは更新後ですが、前の版数は1.5.0

更新履歴は、

https://pkg.go.dev/go.bug.st/serial@v1.6.2?tab=versions

% go get go.bug.st/serial 

で更新してやります、これでめでたくビルドは成功

 

admin

DIライブラリgoogle.wire(@Golang)

DIで依存関係のあるファイルを定義すると、その依存関係を解決したコードを生成するツールです。これだけでは訳がわからなので以下から実例を。

https://www.asobou.co.jp/blog/web/google-wire

はwireのtutorialをベースに記述しています、https://github.com/google/wire

まず何はともあれwireをインストールします、実はwireはコマンド実行で依存関係を記述したコードを作成します。

% go install github.com/google/wire/cmd/wire@latest

インストールするとパスが通っていれば、% wireで実行できるようになります。

//+build wireinject

package main

import "github.com/google/wire"
 
func InitializeEvent() Event {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}
}

ここで、NewEvent, NewGreeter, NewMessageが依存関係のある構造体の名前になります。wire.Bildの引数にこれらの構造体の情報を与えると、依存関係を調べて、依存関係を解消するコードを% wire で生成します。(以下)

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func InitializeEvent() Event {
	message := NewMessage()
	greeter := NewGreeter(message)
	event := NewEvent(greeter)
	return event
}

実は生成されたコードは、元の依存関係を記述しているコード(コメント部分)と同じで、Intialize Eventで呼び出しているだけになります。


func main() {
    e := InitializeEvent()
 
    e.Start()
}

/*
func main() {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
 
    event.Start()
}
*/

実行するには、

% go buildで実行ファイルを作成すれば、作成されたコードをリンクした実行ファイルが作成されるの、そのまま実行可能です。

このtutorialでは単純なケースなので、それほどメリットは感じられませんが依存関係が複雑になればwireを使うメリットがあるように思います。

 

admin

 

 

 

 

第一級オブジェクトと第一級関数(@Golang)

現代の言語で多くは関数を第一級オブジェクトとして扱えます、第一級オブジェクトとは言語の持つ基本的な操作を制限なしに実行できることというのがWikipediaでの解説(以下引用)

第一級オブジェクトファーストクラスオブジェクトfirst-class object)は、あるプログラミング言語において、たとえば生成、代入、演算、(引数戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである。

で、第一級オブジェクトとして関数が扱えれば、それは第一級関数となります。より具体的には以下の使用例を。以下やってることは冗長なだけですが、関数を変数に代入できることは『その言語における基本的な操作を制限なしに使用できる』の一つに該当します。

実際にやっていることは関数の型とポインタを渡しているだけだろうと思いますが。

package main

import (
	"fmt"
	"math"
)

type calif interface{
	calc(i float64) float64
}

type calst struct{
	c calif
}

func (c calst)calc(i float64) float64 {
	return math.Sqrt(i)
}

func main() {
	ci := calst{}
	cal := ci.calc(1)
	ary := []float64{25, 36, 100, 121, 144}
	for _, ft := range ary {
		cal = ci.calc(ft) // set func(calc) to variable(cal)
		fmt.Println(cal)
	}
}

現代の多くの言語では、関数は第一級オブジェクトですが、Rustではそうなっていないようです。意図した理由はあるはずですが、

https://zenn.dev/hinastory/articles/7857427ea390c5

 

admin

 

 

 

contextの基本的な動作(@Golang)

contextは英語の意味は『文脈』ですが、プログラミングにおいては言語に関わらず使われる用語なので、おそらく他に適切な用語はなさそう。

その意味するところはAPI境界を超えて、スレッド間でも共有可能なオブジェクトを提供するといったところかと思います。contextはあたかも一つのスレッドのような動作をするように思います。

contextが一番使われるシーンはJavaでもありますがHttpRequestのようなネットワーク処理でタイムアウトやキャンセルが発生するようなケースに相性が良いから。

以下はcontextでcancel()処理とtimeout()処理、それに何らかの値(key/value pair)を設定してみたもの。

package main

import(
	"context"
	"fmt"
	"time"
)

func main() {
	ctx0 := context.Background()		// to initialize the context

	ctx1, cancel1 := context.WithCancel(ctx0)	// to generate cancel function "cancel1"

	ctx3 := context.WithValue(ctx1, "key1", 99)

	go func(ctx1 context.Context) {
		select {
		case <-ctx1.Done():
			fmt.Println("th1 was canceled")
		}
	}(ctx1)

	ctx2, _ := context.WithTimeout(ctx0, 2*time.Second)	// to set the context to timeout mode

	go func(ctx2 context.Context) {
		select {
		case <-ctx2.Done():
			fmt.Println("th2 was timeouted", "key value : ", ctx3.Value("key1"))
		}
	}(ctx2)

	cancel1()

	time.Sleep(3*time.Second)
}

最後の3秒待ちは値が2秒だとfunc(ctx2)処理が完了する前にプログラムが終了するのでこの値以上が必要です。

channelを使ってclose()処理をしても同じように処理はできますが、

https://zenn.dev/hsaki/books/golang-context/viewer/done

じゃなぜcontextを使うかというと、channelはgoroutine限定ですがcontextは汎用的に使えるからと言うことになるでしょう。

 

admin

 

interfaceの実装(@Golang)

Golangのinterfaceの実装はimplementsのように明示することなく、interfaceで定義したメソッドを全て実装すればinterfaceとしての性質を持ちます。それゆえDependency Injectionも分離したコードが書けるわけですが、他の言語から入ると戸惑いがあるのも事実。

以下の簡単な例で実装時の違いを見てみます。以下のコードではcase 1 ~ 3のいずれかの行を有効にすればコンパイルはできますが、意味はそれぞれ違います。

case 1 : 最も一般的な記述方法、StatStructがinterfaceを実装と認識するのでmain()からinterfaceのメソッドが使えます。

case 2 : 構造体に要素を持たせた場合、この場合はcase 1と同じですが、要素を使わないなら空の構造体にしとくのが自然。

case 3 : このケースとcase 1/2との違いはこの行を記述するだけでStat()の実装がなくともinterfaceを実装したことになること。コンパイルはできますが実行時にStat()が呼べないのでエラーになります。実はこれはDIの手法そのものですが。

package main 

import "fmt"

type Stater interface {
    Stat()
}

//type StatStruct struct {}     // case 1
//type StatStruct struct {a int}    // case 2
type StatStruct struct {Stater}     // case 3

func (StatStruct) Stat() {
    fmt.Println("It's fine today")
}

func main() {
    st := StatStruct{}
    st.Stat() 
}

現実的にはメソッドの実装ではcase 1を使うことが普通だと思います。さらに言えば構造体無しでもStat()を定義すれば動きますが、それはおそらくGoのデザインパターン(ideom)から外れるように思います。

 

admin

DI(Dependency Injection) — (@Golang)

DIはコードをinterfaceを介することでソースコード間の依存性を少なくして、例えばDBの接続種類の入れ替えとかテストの時にスタブに切り替えるとかをコードの修正を最低限で実行できる方法です。結果としてコードの保守性が向上するということになります。

https://free-engineer.life/golang-dependency-injection/

の記事がわかりやすかったのでこれを事例にしてみます。

実際にはVScodeで動かしてみて、パッケージが全てmainでは気持ちが悪いのでパッケージ名は分離しています。以下パッケージ分離版のコードです。

https://github.com/chateight/golang/tree/master/basic/di

<task_usecase(use case.go)のDI実現のキーコード部分>

// Saveメソッドの実装はTaskRepositoryで行っている
type TaskRepositoryInterface interface {
	Save(Task) (Task, error)
}
// structのフィールドにTaskRepositoryInterfaceを持たせる
// こうすることで、CreateTaskメソッドでDBへの保存処理を呼ぶことができる
type TaskUsecase struct {
	repo TaskRepositoryInterface
}

~~途中省略~~

func (usecase TaskUsecase) CreateTask(title string) (task.Task, error) {
	t := task.Task{Title: title}
	task, err := usecase.repo.Save(t)
	return task, err
}

つまりinterfaceを経由した抽象的な呼び出しにより依存先をinterfaceとすることで、CreateTask中ではinterfaceを呼び出し、結果として実装(ここではsave())と分離がされているので、例えば他の実装との入れ替えが簡単にできるということになります。

Golangの場合には明示的にinterfaceをimplementで明示しなくとも、interfaceで定義した関数のフットプリントと同じ関数が実装されていればimplementされたことになるのでDIの記述のようなケースでも、他の例えばJavaなどの比較すると綺麗にコードが分離できます。Golangはクラシックなオブジェクト指向では無いですが、現代的な抽象化手段を持つ言語です。

 

admin