0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Arduino UNO QのLEDマトリクスによるライフゲームの実装

0
Posted at

はじめに

本記事では、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等での推論
等への展開へと次回以降で進めていきたい

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?