はじめに
本記事では、Arduino UNO Qの環境構築が前回の記事にて完了したため、手始めにLチカによる動作確認を実施する。
Arduino UNO QはLinux、STM32のチップをそれぞれ連携しての動作が可能である。
また、エッジAIとしての使用の想定もあるため、
LinuxマイクロプロセッサでのML推論結果をArduino MCU側に伝達し、
アクチュエータやLEDを制御するといった構成が想定される。
Linuxが使用できるため、
①サーバ化とMCUの連携
②TensorFlow等での推論
等への展開も想定し、単体のLEDではなくLEDマトリクスを基にLチカを実装しようと考えた。
折角LEDマトリクスを使用するので動きのあるものを表示しようと考え、今回はLEDマトリクス上でライフゲーム描画を実装した。
目的
Arduino UNO QのLEDマトリクスにライフゲームをリアルタイム表示する。
- Python側でセル状態の計算を行う
- Arduino_RouterBridgeによるRPC通信でフレームデータをArduino側に送信する
- Arduino側でデータを受け取りLEDマトリクスに描画する
準備物
ハードウェア系
- PC(Windows 11)
- Arduino UNO Q
ソフトウェア系
- Arduino IDE
- Python 3(Arduino App Lab)
-
arduino.app_utils(Arduino App Lab 付属モジュール)
アーキテクチャ概要
[Python: life_game.py]
|
| Bridge.call("drawFrame", frame_str)
| (MessagePack RPC over Unix socket)
v
[arduino-router daemon]
|
v
[Arduino MCU: sketch.ino]
|
| matrix.draw(frame)
v
[LEDマトリクス 8×13]
Arduino UNO Qでは、Linuxマイクロプロセッサ(MPU)とArduinoマイクロコントローラ(MCU)がarduino-routerデーモンを介してRPC通信を行う。
Python側でBridge.call("drawFrame", frame_str)を呼び出すと、Arduino側に登録済みのdrawFrame関数が実行される仕組みである。
実装
Arduino側(sketch.ino)
RPCハンドラとLEDマトリクスへの描画を担う。
// RPC通信・LEDマトリクス制御ライブラリのインクルード
#include "Arduino_RouterBridge.h"
#include <Arduino_LED_Matrix.h>
// LEDマトリクスインスタンス
ArduinoLEDMatrix matrix;
// フレーム描画関数(RPC経由で呼び出し)
// pixels: 104文字の '0'/'1' 文字列(8×13グリッド)
void drawFrame(String pixels) {
// 文字列を uint8_t 配列に変換
uint8_t frame[104];
for (int i = 0; i < 104; i++) {
frame[i] = (pixels[i] == '1') ? 1 : 0;
}
// LEDマトリクスへの描画
matrix.draw(frame);
}
// 初期化処理
void setup() {
// LEDマトリクスの初期化・輝度設定
matrix.begin();
matrix.setGrayscaleBits(1);
// RPCブリッジの初期化・関数登録
Bridge.begin();
Bridge.provide("drawFrame", drawFrame);
}
void loop() {}
実装のポイント
String型を使ったデータ受け渡し
Bridge.provideに登録する関数の引数にstd::vector<int>を用いたところ、Arduino_RouterBridgeでの利用が不可であることが判明した。
std::string(C++標準)も同様に動作確認ができなかったため、ArduinoコアのAPIに準拠したString型を採用した。
Python側から104文字の'0'/'1'文字列を送信し、Arduino側でuint8_t[104]に変換している。
loop()が空
Arduino_RouterBridgeが内部でRPCの受信処理を管理するため、loop()への記述は不要である。
Python側(life_game.py)
ライフゲームのロジックとフレーム送信を担う。
from arduino.app_utils import *
import time
import random
import math
# グリッドサイズ定数(LEDマトリクスの行数・列数)
ROWS = 8
COLS = 13
# トーラス境界の有効・無効切り替え(True: 端が繋がる、False: 端は固定死セル)
TOROIDAL = True
# 初期パターン(長さが平方数の1次元リスト、None でランダム初期化)
INITIAL_PATTERN = None
# グライダー(3×3) ― 斜め移動する最小の宇宙船
# INITIAL_PATTERN = [
# 0, 1, 0,
# 0, 0, 1,
# 1, 1, 1,
# ]
# ビーコン(4×4) ― 周期2の振動子
# INITIAL_PATTERN = [
# 1, 1, 0, 0,
# 1, 1, 0, 0,
# 0, 0, 1, 1,
# 0, 0, 1, 1,
# ]
# R-ペントミノ(3×3) ― 長期にわたる複雑な展開パターン
# INITIAL_PATTERN = [
# 0, 1, 1,
# 1, 1, 0,
# 0, 1, 0,
# ]
# アコーン(7×7) ― 5206世代続く長寿命パターン
# INITIAL_PATTERN = [
# 0, 1, 0, 0, 0, 0, 0,
# 0, 0, 0, 1, 0, 0, 0,
# 1, 1, 0, 0, 1, 1, 1,
# 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0,
# ]
# 銀河(Galaxy)(8×8) ― 中心を貫く二重螺旋・180度回転対称パターン
# INITIAL_PATTERN = [
# 0, 0, 1, 1, 0, 0, 0, 0,
# 0, 0, 0, 1, 1, 0, 0, 0,
# 1, 0, 0, 0, 1, 1, 0, 0,
# 1, 1, 0, 0, 0, 1, 1, 0,
# 0, 1, 1, 0, 0, 0, 1, 1,
# 0, 0, 1, 1, 0, 0, 0, 1,
# 0, 0, 0, 1, 1, 0, 0, 0,
# 0, 0, 0, 0, 1, 1, 0, 0,
# ]
# グリッド生成関数
# pattern が None の場合はランダム、指定時はグリッド中央に配置
def make_grid(pattern=None):
if pattern is None:
return [[random.randint(0, 1) for _ in range(COLS)] for _ in range(ROWS)]
# パターン長の平方根を算出し、正方形サイズを確認
n = int(math.isqrt(len(pattern)))
if n * n != len(pattern):
raise ValueError(f"パターンの長さ {len(pattern)} は平方数ではありません")
# 全消灯グリッドへのパターン中央配置
grid = [[0] * COLS for _ in range(ROWS)]
row_offset = (ROWS - n) // 2
col_offset = (COLS - n) // 2
for i in range(n):
for j in range(n):
r, c = row_offset + i, col_offset + j
# グリッド範囲内のセルのみ書き込み
if 0 <= r < ROWS and 0 <= c < COLS:
grid[r][c] = pattern[i * n + j]
return grid
# 次世代グリッド計算関数(Conway's Game of Life ルール適用)
def next_gen(g):
new = [[0] * COLS for _ in range(ROWS)]
for r in range(ROWS):
for c in range(COLS):
# 周囲8近傍の生存セル数をカウント
n = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if (dr, dc) == (0, 0):
continue
if TOROIDAL:
# トーラス境界:端を反対側に折り返す
n += g[(r + dr) % ROWS][(c + dc) % COLS]
else:
nr, nc = r + dr, c + dc
# 固定境界:範囲外は死セルとして扱う
if 0 <= nr < ROWS and 0 <= nc < COLS:
n += g[nr][nc]
# 生存・誕生ルールの適用
if g[r][c]:
new[r][c] = 1 if n in (2, 3) else 0
else:
new[r][c] = 1 if n == 3 else 0
return new
# グリッドを104文字の '0'/'1' 文字列に変換
def grid_to_str(g):
return ''.join(str(g[r][c]) for r in range(ROWS) for c in range(COLS))
# 初期グリッドの生成
grid = make_grid(INITIAL_PATTERN)
# メインループ(App.run により繰り返し呼び出し)
def loop():
global grid
# 現在グリッドのフレーム送信
Bridge.call("drawFrame", grid_to_str(grid))
next_grid = next_gen(grid)
# 全滅または静止状態でのリセット
if next_grid == grid or not any(cell for row in next_grid for cell in row):
grid = make_grid(INITIAL_PATTERN)
else:
grid = next_grid
time.sleep(0.5)
App.run(loop)
実装のポイント
App.runの動作
App.run(loop)の第1引数(user_loop)に指定した関数がArduinoのloop()に相当し、繰り返し呼び出される。
初期グリッドの生成はモジュールロード時に1度だけ実行し、以降はloop()内で世代更新を行う。
Conway's Game of Life のルール
| 条件 | 次世代 |
|---|---|
| 生存セル・近傍2〜3個 | 生存 |
| 生存セル・近傍2〜3個以外 | 死滅 |
| 死セル・近傍ちょうど3個 | 誕生 |
トーラス境界(TOROIDAL)
TOROIDAL = Trueの場合、グリッドの端を反対側に折り返して近傍を計算する。
TOROIDAL = Falseの場合、範囲外のセルを死セルとして扱うため、端のセルは消えやすくなる。
自動リセット
全滅(生存セル0個)または静止状態(世代間で変化なし)になった場合、INITIAL_PATTERNに基づき初期状態に戻る。
初期パターンについて
INITIAL_PATTERNに正方形の1次元リストを指定することで、任意の初期状態を設定できる。
指定されたパターンはグリッド中央に配置され、それ以外のセルは消灯状態となる。
使いたいパターンのコメントアウトを外すことで切り替え可能である。
| パターン名 | サイズ | 特徴 |
|---|---|---|
| グライダー | 3×3 | 斜め方向に移動する最小の宇宙船 |
| ビーコン | 4×4 | 2状態を繰り返す周期2の振動子 |
| R-ペントミノ | 3×3 | 長期にわたるカオス的な展開 |
| アコーン | 7×7 | 長寿命パターン |
| 銀河(Galaxy) | 8×8 | 中心を貫く二重螺旋・180度回転対称 |
動作パラメータ
| パラメータ | デフォルト値 | 説明 |
|---|---|---|
TOROIDAL |
True |
トーラス境界の有効・無効 |
INITIAL_PATTERN |
None |
初期パターン(None でランダム) |
time.sleep(0.5) |
0.5秒 |
フレーム更新間隔 |
実行結果
上記スクリプト実行結果は以下の動画の通りである。また、各動作パラメータは以下の表の通りである。
| パラメータ | 設定値 |
|---|---|
TOROIDAL |
True |
INITIAL_PATTERN |
(アーコン) |
time.sleep(0.5) |
0.5秒 |
まとめ
Lチカもかねて、MPU+MCUでの連携しての処理を確認することができた。
冒頭にも説明したように、
①サーバ化とMCUの連携
②TensorFlow等での推論
等への展開へと次回以降で進めていきたい

