バイスタンプブック(スタンプ帳)をつくる

今回は自身のバイスタンプ研究の集大成として、25種のバイスタンプとして動作可能な『バイスタンプブック』を作ってみました。バイスタンプの『スタンプ帳』というコンセプトです。

製作経緯

いわゆる『オールインワン』系のアイテムです。この手のアイテムは、何かしら改造玩具を作るときには割と誰でも思い付いてしまうネタなのと、あとシンプルに作業が大変なので、自分の場合は基本的には避けるようにしています。

そう言いながらも過去にそれなりの数オールインワン系を作っている(『ジーニアスフルボトル』、『ネオディケイド/ディエンドライドウォッチ』、『ZXライドウォッチ』、『プログライズライター』、『逢魔降臨暦ワンダーライド電子ブック』)のは、この手のアイテムが、観てくれる人に面白さが伝わりやすいからだと思います。

とは言え、先述のとおり基本的には作りたくないものなので、自分の中では「オールインワンであることに納得性を持たせられるアイデア」を思いつかない限りは作らないようにしています(←納得性については、自分のどの作品にも言えることですが)。

で、前置きが長くなりましたが、今回の『バイスタンプブック』。バイスタンプについてはバンダイさんがかなり玩具展開を頑張っていて、多くのアイテムでバイスタンプの個別認識が可能になっています(『リバイスドライバー』『ガンデフォン』『バリッドレックス』『ローリング』『サンダーゲイル』『デモンズドライバー』)。そのため、「一つのバイスタンプで複数のバイスタンプとして動作できる」というオールインワン系のアイテムは存在価値があると思われました(←たくさんのバイスタンプをガチャガチャ持ち歩く必要がないように)。ただ、複数のバイスタンプを一つにまとめるための良いアイデア(受け皿)がない。『ディケイド』や『逢魔降臨暦』のような。

複数バイスタンプをまとめるためのアイデアと、前作『セイバー』のワンダーライドブックの活用を並行して考えていたところ、『スタンプ帳』という形にすればバイスタンプの力が一つにまとまっていることの説明がつくのではないか、と思い、自分の中で納得感が得られましたので、制作に着手することにしました。

 

特徴

強化系、景品を除く基本の25種のバイスタンプとして動作します。ブック側の音声とグラフィックだけでなく、赤外線通信の内容も変えているので、

リバイスドライバー側の発光、バリッドレックス側の音声、デモンズドライバー側の音声・グラフィックなどもきちんと変化するようになっています。

もちろん、オーインバスター、ツーサイドライバーにも対応しています。

リベラドライバーにも使うことはできますが、横向きになっちゃったりスタンプ部分がなかったりであんまり向いていません。

バイスタンプ共通の『押印』という機能は、『スタンプ帳』というモノから考えると本来不要なのですが、『リバイス』の作品的に、変身シーケンスの中に『押印』のプロセスを入れるのは必須かなと思いましたので、押印できるようにしてみました。スタンプの絵柄を変化させるのは流石に難しかったので、汎用性を考えて、『オーインバスター』で押印できる、リバイスのライダーズクレストの絵柄にしています。

『スタンプ帳』は、一種の図鑑みたいなものなので、『ガンデフォン』に搭載されている、バイス先生のバイスタンプ講座の説明音声がマッチするかなと思いました。ということで、本を開いているときにはこの音声が聞けるようにしてみました。

本状のアイテムということで、ワンダーライドブックのように、ベルトの操作で表紙が開くようなギミックも検討はしました。ただその場合、レバー操作時に物理的にスイッチを押せるリバイスドライバーは良くても、赤外線通信しか利用できないオーインバスター、ツーサイドライバー、デモンズドライバーなどでの使用も想定すると、ページを開くギミックを搭載しようとするとかなり大掛かりなものになり実装困難になることが予想されたため(←ソレノイドとかの実装が必要)、今のようなマジックミラーで表紙から中が透けて見える形の実装にすることにしました。

ちなみにこのマジックミラーは、『大乱闘スマッシュブラザーズガシャット』を作成した際に、「いずれ何かに使えるかも」と思って、ベースとなった『マイティブラザーズXXガシャット』から取って保存しておいた部品になります。6年越しに使用することになりました。

 

ハードウェア解説

今回使用した具材の紹介をしながら、ハード側の作成のポイントをご説明します。

 

今回、メインマイコンとして初めてRaspberry Pi Picoを採用しました。理由は2つで、今回使用したかったLEDモジュール(後述)がPico専用っぽかったこと(←実際はそうではなかった)と、もう一つは、MicroPythonでの開発を試してみたかったからです。このあたりのことは「ソフトウェア解説」のところで改めてご説明します。

 

160個のNeoPixel(WS2812)が搭載されたモジュールです。実は「生物の押印」を表現するにあたり、このモジュールを使ってデモンズドライバーのようにドット絵で生物を表現するか、電子ペーパー搭載モジュール(M5 Core-Ink)を使ってスタンプの画像データを表現するかは、結構悩みました。実際、初めは「スタンプ帳」としての表現を重視して後者の方針で制作を進めていた(←電子ペーパーの方が電源OFF時もスタンプの絵柄の表示が残るから)のですが、試しに前者のアプローチでドット絵をアニメーションさせてみたら、そちらの方が断然面白かったので、こちらのモジュールで制作を進めることにしました。

このモジュールを使用するときの工夫ですが、これをそのまま使うと眩しくてドット絵として見辛くなるので、普通の家庭用インクジェットプリンタで白ラベルシートに印刷した水玉模様(?)のシールを上から貼り付けています。

 

私の作品ではお馴染みのMP3音声再生モジュールです。Raspberry Pi Picoでも特に問題なく動作してくれました。なお、スピーカーについては、こちらも私の作品では定番のこちらを使用しています。

 

『リバイス』玩具との通信に必須の赤外線LEDです。赤外線通信については、リバイスドライバーとの連動に特化するなら頑張らなくても良い(←食玩バイスタンプと同じ方式にする)のですが、その他のオーインバスターやツーサイドライバー、デモンズドライバーなどとの連動を実現させようと思うと、頑張って実装する必要があります。

 

動作させるためのバッテリーです。大量のLEDを使用するので可能な限り容量大きめにしました。

 

スタンプの起動や切り替えに使用するための3-Wayスイッチです。あとは他にも、ちょこちょこと、コネクタやディテクタスイッチなどを使用しています。

 

なお、外装の大部分は3Dプリンタで自作していますが、一部は食玩のワンダーランドブックの部品を使用しています。

3Dプリントのフィラメントは上記を使用しました。

 

押印部分のマグネットは、『オーインバスター』のマグネット部をドライヤーで温めて剥がして移植しています。

口にあたる部分に赤外線送受信部とリバイスドライバーのレバー操作時に飛び出してくる部分を受ける穴を持ってくることで、ドライバーセット用の凸部と押印部分を兼用させているのが地味な設計ポイントです。これで、出来るだけ本の形状を崩さずに押印できるようになりました。

 

回路図はこんな感じです。一つ注意は、DFPlayer(MP3プレイヤー)とマトリクスLEDへの電力供給を行うときは、Raspberry Pi PicoのVSYS端子から引っ張ろうとすると電力が足りなくて全然動作してくれないので、VBUSの方から引っ張る必要があります。

実際の配線はこんな感じになりました。

 

ソフトウェア解説

今回の大きなポイントは、Raspberry Pi Picoの採用に伴い、使用言語をC/C++ベースのArduino言語からMicroPythonへ移行したことです。

移行の理由ですが、元々本業の方ではPythonを使うことが多かったのですが、最近コーディングをすることがほとんどなくなってしまったためです。自分ではコードは書かないけど、ある程度は他人のコードを見てコメントはできないといけない。というわけで、これまでは趣味の時間はC/C++ベースでやってきたけれど、趣味の時間の方でもPythonをメインに使用することで、本業で優先されるスキルの劣化を少しでも防ごうというのが動機になります。逆にC/C++の方のスキルが少しずつ劣化していくことにはなりますが、これはまあ致し方ない。

で、初めてMicroPythonを使い始めたわけですが、最初は「組込用のPythonって、どれぐらい普通のPythonと同じように扱えるんだろう?」というのがとても不安でした。が、自分が軽く触った範囲では全然問題なかったです。普通のPythonと同じように扱える。利用頻度が低めのちょっと便利な関数まではフォローされていない、ぐらいです。

次に必要だったのは、定番モジュールの利用可否確認です。Arduino向けには大抵ライブラリがセットで提供されますが、Raspberry Pi Picoは比較的新参者なので、ライブラリが揃っているかは不明でした。私の場合必須だったのは、発光制御のためのNeoPixel(WS2812)と、音声制御のためのDFPlayerです。幸い、前者についてはこちらでWS2812のライブラリを公開してくれており、後者についてはこちらの方の記事を参考に自分でライブラリを作ることができました。このあたりについては、個別に記事にしていますので、必要に応じてご参照ください。

あとは今回の作品のための独自の作り込みです。まず、バイスタンプの赤外線通信の部分については、自分の過去作品でArduino言語で一部は書いていたので、それをベースに足りない部分を補いながら移植しました。実装にあたっては、赤外線通信のコマンドの仕様とシーケンスの理解が必要になります。例えばこんな感じです。

このあたりの仕様は全部自分で解析しました。こちらの記事も併せて読んで頂くのが良いと思います。全てのシーケンスを掲載するのは大変なので、各シーケンスに出てくるコマンドの意味だけ、私が調べた範囲内で掲載しておきます。誤りが含まれている可能性もありますので、その点についてはご了承くださいませ。あと図が小さくてすみません。

赤外線通信が実装できたら、次はそれを踏まえての状態管理です。実際のバイスタンプの動作はさておき、今作では以下のように状態を管理することにしました。

これが実装できれば、あとは状態変化が発生したことをトリガーとして音声やLEDの発光パターンを切り替えればOK、ということになります。

音声についてはライブラリが使えるようになればそこまで大変ではないのですが、大変なのはLEDの発光パターンの制御です。まず、生物の絵を表現できるように、狙った位置のLEDを、狙った色で発光させないといけない。これについては、発光の設定が多少楽になるように、人が見る形で位置・色を指定すれば、適切に設定値(配列)を出力してくれるプログラムを別に作りました。あとは、デモンズドライバーのドット絵を参考に、25個分の生物の絵を地道に作成しました。

そして一番難しいのが、アニメーションの作成です。自分がやりたいアニメーションをドンピシャでライブラリ化してくれている、なんてことはありませんでしたので、ここは自分で頑張って考えてアニメーションのアルゴリズムを実装しました。何とか形にはなったかな、と思います。一つハマったのは、このLEDによるアニメーションと先の赤外線通信のタスクをマルチスレッドで動作させると、何故か全体の動作がフリーズしてしまう、という現象が起こったことです。この現象については、幸いにも先人の解決策を真似てみることで解決しました。

以上を踏まえて最終的に実装したソースコードは、本記事の最後に掲載しています。Arduino言語のときはほぼほぼ一つのファイルで作成してしまうことが多かったのですが、せっかくPythonに切り替えたので、もう少しファイルを分けて、オブジェクト指向言語っぽく書くようにしてみました。正しいクラス図の描き方は知りませんが、設計のイメージはこんな感じです。

 

まとめ

以上、『バイスタンプブック』のご紹介でした。自分のバイスタンプ研究の集大成として、「バンダイの中の人以外だと、これを(少なくとも最初に)作れるのは自分しかおるまい」と思えるものを作ったつもりです。

今回、構想から出来上がりまで結局半年ぐらい掛かってしまいました。要因としては、新しいマイコンを採用したことでの学習コストだったり、そもそものボリュームが多過ぎて玩具音声の録音が死ぬほど大変だったり、といったところもあるのですが、やっぱり大きいのは、リバイス玩具の構造の複雑さだと思います。

至る所で採用されている赤外線通信が、とにかく一筋縄ではいかない。バイスタンプの認識が、サンダーゲイルで上手くいったかと思えば、ローリングでは上手くいかない。ツーサイドライバーで上手くいったかと思えば、リベラドライバーでは上手くいかない。そんなことが至る所にあり、とにかく通信仕様の読み解きが大変でした。結局、私も未だ完全には理解しきれていません。

リバイス玩具で採用された赤外線通信方式は本当に優れていて、玩具のデザインを邪魔しにくい上に、コスト減、秘匿性、拡張性を備えた、本当に素晴らしいアイデアだと思います。玩具も素晴らしいものが多くて、特にデモンズドライバーは個人的には大傑作だと思っています。よくもあれだけのバイスタンプに対応させたなと。本当にもう、リバイス玩具の開発エンジニアの方々にはただただ頭が下がります。

この『バイスタンプブック』で、個人的には『リバイス』の玩具改造を終えるに相応しいものができたかなと思いますので、またゆっくり、別の作品作りに取り掛かろうかなと思います。次は何にしよう、ボトルマンか、ドンブラか、次のライダーか。

 

ソースコード

ここからは必要な方向けです。Arduino/ESP32ベースのときは基本的に1ファイルで全部書いていたのですが、今回は8ファイルに分けています。

再掲ですが、以降の8個のファイルの関係性は上図のようになっておりますので、上図を頭において読み進めて頂くと良いと思います。

common.py

複数のファイル(クラス)から参照される定数の定義をまとめたファイルです。

STATE_INIT                 =  0
STATE_ACTIVATE             =  1
STATE_GENOME_CHANGE        =  2
STATE_LECTURE              =  3
STATE_STAMP                =  4
STATE_SET                  =  5
STATE_CHANGE               =  6
STATE_DRIVER_GENOME_CHANGE =  7
STATE_FINISH_READY         =  8
STATE_FINISH               =  9
STATE_REMIX_READY          = 10
STATE_REMIX                = 11
STATE_WEAPON_READY         = 12
STATE_WEAPON               = 13
STATE_WEAPON_GENOME_CHANGE = 14
STATE_WEAPON_CONFIRM       = 15

COMMAND_NONE    = 0
COMMAND_SET_TOP = 1
COMMAND_DRIVER_LIGHT_OFF = 2
COMMAND_STAMP_LIGHT_OFF  = 3

SIGN_ZERO  =  0
SIGN_PLUS  =  1
SIGN_MINUS = -1

main.py

メインプログラムで、個人的にはピンアサインとか、ハード周りを扱うためのクラスのイメージです。各GPIOでどのような値の変化が起これば状態がどのように変わるか、を管理する役割でもあります。

“main.py”の名前でPicoに保存することで、PC不要で、Picoに電力を流せばこのプログラムが自動的に動き出すようになります(スタンドアロン化)。ただ、一度”main.py”をPicoに保存してしまうと、以降、PCに繋いだときのプログラムの書き換えがややこしいことになるので、できれば”main.py”をPicoに保存するのは、プログラムのデバッグなども全て終わった、最後の最後にした方が良いと思います。

マルチスレッドで動作するように記述していて、一つのスレッドはリバイスドライバーなどとの赤外線通信に集中させるようにしています。200ms間隔ぐらいで絶えず赤外線通信をさせておかないといけないので。

import utime
import machine
import _thread
# マルチスレッド処理で発生するクラッシュを防ぐためのライブラリ
# 参照: https://bytesnbits.co.uk/multi-thread-coding-on-the-raspberry-pi-pico-in-micropython/
import gc

from common import *
from state_manager import StateManager
from leds_controller import LedsController
from sound_controller import SoundController

PIN_MP3_TX = 4
PIN_MP3_RX = 5
PIN_LED    = 6
PIN_BTN_NEXT   = machine.Pin(7, machine.Pin.IN, machine.Pin.PULL_UP)
PIN_BTN_TOP    = machine.Pin(8, machine.Pin.IN, machine.Pin.PULL_UP)
PIN_BTN_PREV   = machine.Pin(9, machine.Pin.IN, machine.Pin.PULL_UP)
PIN_BTN_BACK   = machine.Pin(10, machine.Pin.IN, machine.Pin.PULL_UP)
PIN_BTN_INSIDE = machine.Pin(11, machine.Pin.IN, machine.Pin.PULL_UP)
PIN_IR_RX = machine.ADC(26) # Analogue Input
PIN_IR_TX = machine.Pin(26, machine.Pin.OUT)

ON  = 0 # LOW
OFF = 1 # HIGH

btn_top    = prev_btn_top  = OFF
btn_prev   = prev_btn_prev = OFF
btn_next   = prev_btn_next = OFF
btn_back   = prev_btn_back = OFF
btn_inside = OFF

STAMP_CHANGE_READY_TIMEOUT_MS   = 15000
CHANGE_TURN_DRIVER_LIGHT_OFF_MS = 16000
FINISH_TURN_DRIVER_LIGHT_OFF_MS =  9000
REMIX_TURN_DRIVER_LIGHT_OFF_MS  =  8000
WEAPON_TIMEOUT_MS = 1200

IR_SEND_LIMIT_MS = 1000

#########################################################

SOUND_VOLUME = 22
sound_controller = SoundController(PIN_MP3_TX, PIN_MP3_RX, SOUND_VOLUME)
leds_controller  = LedsController(PIN_LED)
state_manager    = StateManager(sound_controller, leds_controller)

#########################################################

LEN_IR_DATA  = 8

def send_start():
    PIN_IR_TX.value(1)
    utime.sleep_us(6000)
    PIN_IR_TX.value(0)
    utime.sleep_us(500)

def send_bit_0():
    PIN_IR_TX.value(1)
    utime.sleep_us(1500)
    PIN_IR_TX.value(0)
    utime.sleep_us(500)

def send_bit_1():
    PIN_IR_TX.value(1)
    utime.sleep_us(500)
    PIN_IR_TX.value(0)
    utime.sleep_us(1500)

def send_ir(data):
    PIN_IR_TX = machine.Pin(26, machine.Pin.OUT)
    send_start()
    # dataを左端から順に送信する
    for i in range(LEN_IR_DATA):
        if (data >> (LEN_IR_DATA-1-i)) & 1 == 1:
            send_bit_1()
        else:
            send_bit_0()
    # 送信結果を表示する
    print("Stamp -> Driver: {:08b} / {:d}".format(data, data))

def ir_rx_tx_task():

    IR_THRESHOLD_FOR_STAMP = 15000
    READ_INTERVAL_MICROS = 2000 # 1990から
    SEND_IR_DELAY_MS = 50
    RESET_MS = 350

    reset_start_point_ms = 0

    is_identified = False

    while True:
        # 基本的にはドライバーからの信号を受信モードで待つ
        PIN_IR_RX = machine.ADC(26) # Analogue Input

        reset_start_point_ms = utime.ticks_ms()
        while PIN_IR_RX.read_u16() < IR_THRESHOLD_FOR_STAMP:
            # read_u16()は、0〜65535の値を返す。1回あたりの実行時間は0.013ms = 13us
            now_ms = utime.ticks_ms()
            if now_ms - reset_start_point_ms > RESET_MS:
                state = state_manager.get_state()
                stamp_id = state_manager.get_stamp_id()
                if state == STATE_INIT:
                    if now_ms - state_manager.get_state_change_point_ms() < IR_SEND_LIMIT_MS:
                        # STATE_INITのときは短時間でIR送信を打ち切った方が連続認識させやすい
                        send_ir(stamp_id)
                        PIN_IR_RX = machine.ADC(26) # 送信後は受信モードに戻す
                        reset_start_point_ms = utime.ticks_ms()
                elif state in [STATE_ACTIVATE, STATE_LECTURE, STATE_GENOME_CHANGE, STATE_STAMP]:
                    send_ir(stamp_id)
                    PIN_IR_RX = machine.ADC(26) # 送信後は受信モードに戻す
                    reset_start_point_ms = utime.ticks_ms()
                else:
                    state_manager.state_change(STATE_INIT)
                    is_identified = False

        # 閾値を越えた(ONになった)ので、5500us待つ
        utime.sleep_us(5500)

        # 送受信データは丁度1byteなので、1byteの変数に受信結果を収める
        recv_data = 0
        if PIN_IR_RX.read_u16() >= IR_THRESHOLD_FOR_STAMP:
            # ここでも閾値を超えていれば、スタンプから信号が来たとみなし、およそ2000usごとの読み取りを開始する
            for i in range(LEN_IR_DATA):
                utime.sleep_us(READ_INTERVAL_MICROS)
                if PIN_IR_RX.read_u16() < IR_THRESHOLD_FOR_STAMP:
                    # ON:1500us, OFF: 500us ... 0
                    # ON: 500us, OFF:1500us ... 1
                    # なので、2000u秒ごとのサイクルで1000usの位置がONなら0、OFFなら1となる
                    recv_data = recv_data | (1 << (LEN_IR_DATA-1-i));

            # ドライバーから受け取った信号を表示する
            print("Stamp <- Driver: {:08b} / {:d}".format(recv_data, recv_data))

            send_data = 0

            if recv_data in [70, 101,102,105,111,112,113,114]:  # 各種ドライバー/デバイス スタンプセット待機(70はサンダーゲイル対応)
                send_data = stamp_id # バイスタンプ    識別ID
            elif recv_data == 103:   # リベラドライバー   スタンプセット待機
                if not is_identified:
                    send_data = stamp_id # バイスタンプ  識別ID
                    is_identified = True
                else:
                    send_data = 151  # バイスタンプ    スタンプセット要求
                    state_manager.state_change(STATE_WEAPON_READY)
            elif recv_data == 121:   # リバイスドライバー レバー操作
                send_data = stamp_id # バイスタンプ    識別ID
            elif recv_data == 129:   # 共用            シーケンス終了通知
                send_data = 130      # バイスタンプ    シーケンス終了(通常)応答
            elif recv_data == 140:   # 共用        シーケンス開始要求
                send_data = 150      # バイスタンプ    シーケンス開始応答
            elif recv_data == 143:   # 共用              天面ボタン操作応答
                send_data = 128      # バイスタンプ    スタンプセット継続応答
            elif recv_data == 151:   # リバイスドライバー スタンプセット要求
                send_data = 201      # バイスタンプ    スタンプセット応答
                state_manager.state_change(STATE_SET)
            elif recv_data == 152:   # リバイスドライバー 変身開始要求
                send_data = 202      # バイスタンプ    変身開始応答
                state_manager.state_change(STATE_CHANGE)
            elif recv_data == 153:   # リバイスドライバー 必殺技待機開始要求
                send_data = 203      # バイスタンプ    必殺技待機開始応答
                state_manager.state_change(STATE_FINISH_READY)
            elif recv_data == 154:   # リバイスドライバー 必殺技開始要求
                send_data = 204      # バイスタンプ    必殺技開始応答
                state_manager.state_change(STATE_FINISH)
            elif recv_data == 154:   # リバイスドライバー 必殺技開始要求
                send_data = 204      # バイスタンプ    必殺技開始応答
                state_manager.state_change(STATE_FINISH)
            elif recv_data == 155:   # リバイスドライバー リミックス待機開始要求
                send_data = 205      # バイスタンプ    リミックス待機開始応答
                state_manager.state_change(STATE_REMIX_READY)
            elif recv_data == 156:   # リバイスドライバー リミックス開始要求
                send_data = 206      # バイスタンプ    リミックス開始応答
                state_manager.state_change(STATE_REMIX)
            elif recv_data == 158:   # オーインバスター 必殺技待機待機開始要求 / ツーサイドライバー 変身待機開始要求
                send_data = 208      # バイスタンプ   上記応答
                state_manager.state_change(STATE_WEAPON_READY)
            elif recv_data == 168:   # オーインバスター 必殺技開始要求 / ツーサイドライバー 変身開始要求 / オストデルハンマー 必殺技待機開始要求
                send_data = 218      # バイスタンプ   上記応答
                state_manager.state_change(STATE_WEAPON)
            elif recv_data == 174:   # 共用     バイスタンプ点滅発光(遅)要求
                send_data = 224      # バイスタンプ バイスタンプ点滅発光(遅)応答
            elif recv_data == 175:   # 共用     バイスタンプ点滅発光(速)要求
                send_data = 225      # バイスタンプ バイスタンプ点滅発光(速)応答
            elif recv_data == 186:   # 共用     バイスタンプ多色発光要求
                send_data = 236      # バイスタンプ バイスタンプ多色発光応答
            elif recv_data == 199:   # 共用     バイスタンプ消灯要求
                send_data = 249      # バイスタンプ バイスタンプ消灯応答
            elif recv_data == 207:   # リバイスドライバー 発光終了応答
                send_data = 128      # バイスタンプ スタンプセット継続応答
            elif recv_data == 233:   # リバイスドライバー 点滅発光応答
                send_data = 128      # バイスタンプ スタンプセット継続応答
            else:
                command_from_stamp = state_manager.get_command_from_stamp()
                if command_from_stamp == COMMAND_SET_TOP:
                    send_data = 133  # バイスタンプ 天面ボタン操作要求
                    state_manager.set_command_from_stamp(COMMAND_NONE)
                elif command_from_stamp == COMMAND_DRIVER_LIGHT_OFF:
                    send_data = 157  # バイスタンプ リバイスドライバー発光終了要求
                    state_manager.set_command_from_stamp(COMMAND_NONE)
                elif command_from_stamp == COMMAND_STAMP_LIGHT_OFF:
                    send_data = 129  # バイスタンプ バイスタンプ発光終了通知
                    state_manager.set_command_from_stamp(COMMAND_NONE)
                else:
                    send_data = 128  # バイスタンプ スタンプセット継続応答

            utime.sleep_ms(SEND_IR_DELAY_MS)

            # 読み取ったデータに対するレスポンスをドライバーヘ伝える
            send_ir(send_data)

#########################################################

# IR信号の送受信は別のコアで動かす
_thread.start_new_thread(ir_rx_tx_task, ())

# メイン処理
while True:

    btn_next   = PIN_BTN_NEXT.value()
    btn_top    = PIN_BTN_TOP.value()
    btn_prev   = PIN_BTN_PREV.value()
    btn_back   = PIN_BTN_BACK.value()
    btn_inside = PIN_BTN_INSIDE.value()

    state = state_manager.get_state()

    if state == STATE_INIT:
        if prev_btn_top == OFF and btn_top == ON:
            if btn_inside == ON:
                state_manager.state_change(STATE_ACTIVATE)
            else:
                state_manager.state_change(STATE_LECTURE)
        elif prev_btn_prev == OFF and btn_prev == ON:
            state_manager.stamp_change(-1)
            state_manager.state_change(STATE_GENOME_CHANGE)
        elif prev_btn_next == OFF and btn_next == ON:
            state_manager.stamp_change(1)
            state_manager.state_change(STATE_GENOME_CHANGE)
        elif prev_btn_back == OFF and btn_back == ON:
            state_manager.state_change(STATE_STAMP)

    elif state == STATE_ACTIVATE:
        if prev_btn_top == ON and btn_top == OFF:
            state_manager.state_change(STATE_INIT)

    elif state == STATE_GENOME_CHANGE:
        if prev_btn_prev == ON and btn_prev == OFF:
            state_manager.state_change(STATE_INIT)
        elif prev_btn_next == ON and btn_next == OFF:
            state_manager.state_change(STATE_INIT)

    elif state == STATE_LECTURE:
        if prev_btn_top == ON and btn_top == OFF:
            state_manager.state_change(STATE_INIT)

    elif state == STATE_STAMP:
        if prev_btn_top == OFF and btn_top == ON:
            state_manager.state_change(STATE_ACTIVATE)
        elif prev_btn_prev == OFF and btn_prev == ON:
            state_manager.stamp_change(-1)
            state_manager.state_change(STATE_GENOME_CHANGE)
        elif prev_btn_next == OFF and btn_next == ON:
            state_manager.stamp_change(1)
            state_manager.state_change(STATE_GENOME_CHANGE)

    elif state == STATE_SET:
        pass

    elif state in [STATE_CHANGE, STATE_FINISH, STATE_REMIX_READY, STATE_REMIX]:
        if prev_btn_prev == OFF and btn_prev == ON:
            state_manager.stamp_change(-1)
            state_manager.state_change(STATE_DRIVER_GENOME_CHANGE)
        elif prev_btn_next == OFF and btn_next == ON:
            state_manager.stamp_change(1)
            state_manager.state_change(STATE_DRIVER_GENOME_CHANGE)

    elif state == STATE_DRIVER_GENOME_CHANGE:
        if prev_btn_prev == ON and btn_prev == OFF:
            state_manager.state_change(STATE_CHANGE)
        elif prev_btn_next == ON and btn_next == OFF:
            state_manager.state_change(STATE_CHANGE)

    elif state == STATE_FINISH_READY:
        if prev_btn_top == ON and btn_top == OFF:
            # ドライバーにリミックス要求を出させるためのトリガー
            state_manager.set_command_from_stamp(COMMAND_SET_TOP)
        elif prev_btn_prev == OFF and btn_prev == ON:
            state_manager.stamp_change(-1)
            state_manager.state_change(STATE_DRIVER_GENOME_CHANGE)
        elif prev_btn_next == OFF and btn_next == ON:
            state_manager.stamp_change(1)
            state_manager.state_change(STATE_DRIVER_GENOME_CHANGE)

    elif state == STATE_WEAPON_READY:
        if prev_btn_top == OFF and btn_top == ON:
            # 必殺承認要求を出させるためのトリガー
            state_manager.set_command_from_stamp(COMMAND_SET_TOP)
            state_manager.state_change(STATE_WEAPON_CONFIRM)
        elif prev_btn_prev == OFF and btn_prev == ON:
            state_manager.stamp_change(-1)
            state_manager.state_change(STATE_WEAPON_GENOME_CHANGE)
        elif prev_btn_next == OFF and btn_next == ON:
            state_manager.stamp_change(1)
            state_manager.state_change(STATE_WEAPON_GENOME_CHANGE)

    elif state == STATE_WEAPON:
        if prev_btn_prev == OFF and btn_prev == ON:
            state_manager.stamp_change(-1)
            state_manager.state_change(STATE_WEAPON_GENOME_CHANGE)
        elif prev_btn_next == OFF and btn_next == ON:
            state_manager.stamp_change(1)
            state_manager.state_change(STATE_WEAPON_GENOME_CHANGE)

    elif state == STATE_WEAPON_GENOME_CHANGE:
        if prev_btn_prev == ON and btn_prev == OFF:
            state_manager.state_change(STATE_WEAPON_READY)
        elif prev_btn_next == ON and btn_next == OFF:
            state_manager.state_change(STATE_WEAPON_READY)

    elif state == STATE_WEAPON_CONFIRM:
        if prev_btn_top == ON and btn_prev == OFF:
            state_manager.state_change(STATE_WEAPON_READY)

    ##### 時間経過処理 #####
    state = state_manager.get_state()
    timer_start_point_ms = state_manager.get_timer_start_point_ms()
    is_driver_light_on = state_manager.get_is_driver_light_on()

    if state == STATE_STAMP and (utime.ticks_ms() - timer_start_point_ms) > STAMP_CHANGE_READY_TIMEOUT_MS:
        state_manager.state_change(STATE_INIT)
    elif state == STATE_CHANGE and is_driver_light_on and  (utime.ticks_ms() - timer_start_point_ms) > CHANGE_TURN_DRIVER_LIGHT_OFF_MS:
        state_manager.set_command_from_stamp(COMMAND_DRIVER_LIGHT_OFF)
        state_manager.set_is_driver_light_on(False)
    elif state == STATE_FINISH and is_driver_light_on and (utime.ticks_ms() - timer_start_point_ms) > FINISH_TURN_DRIVER_LIGHT_OFF_MS:
        state_manager.set_command_from_stamp(COMMAND_DRIVER_LIGHT_OFF)
        state_manager.set_is_driver_light_on(False)
    elif state == STATE_REMIX and is_driver_light_on and (utime.ticks_ms() - timer_start_point_ms) > REMIX_TURN_DRIVER_LIGHT_OFF_MS:
        state_manager.set_command_from_stamp(COMMAND_DRIVER_LIGHT_OFF)
        state_manager.set_is_driver_light_on(False)
    elif state == STATE_WEAPON and (utime.ticks_ms() - timer_start_point_ms) > WEAPON_TIMEOUT_MS:
        state_manager.set_command_from_stamp(COMMAND_STAMP_LIGHT_OFF)
        state_manager.state_change(STATE_WEAPON_READY)

    ##### LEDマトリクス処理 #####
    state_manager.control_leds()

    ##### 処理状態の保存 #####

    prev_btn_next = btn_next
    prev_btn_top  = btn_top
    prev_btn_prev = btn_prev
    prev_btn_back = btn_back

    # これを入れておかないと、メインスレッドと赤外線処理スレッドがクラッシュする
    gc.collect()

    utime.sleep_ms(10)

state_manager.py

バイスタンプブックが今どういう状態になっているのか、どのバイスタンプになっているのか、を管理する役割です。このクラスのインスタンスは”main.py”で生成されて、そのときにLEDマトリクスの管理者である”LedsController”と音声の管理者である”SoundController”のインスタンスを”main.py”から預かっていて、「何から何に状態が変わったか」をそれらに渡すことで、音を出してもらったり、LEDを発光させてもらったりしています。

import utime
import _thread

from common import *
from leds_controller import LedsController
from sound_controller import SoundController

STATE_STR = [
        "INIT", "ACTIVATE", "GENOME_CHANGE", "LECTURE", "STAMP",
        "SET", "CHANGE", "DRIVER_GENOME_CHANGE", "FINISH_READY", "FINISH",
        "REMIX_READY", "REMIX", "WEAPON_READY", "WEAPON", "WEAPON_GENOME_CHANGE",
        "WEAPON_CONFIRM"
    ]

STAMP_INDEX_ID_MAP = [
         1, 13, 16, 74, 14, # レックス、イーグル、マンモス、プテラ、メガロドン
        23,  6, 11,  9,  4, # ライオン、ジャッカル、コング、カマキリ、ブラキオ、
        46,  3,  8, 76, 39, # ネオバッタ、カンガルー、カジキ、トイザウルス、バット、
        40, 41, 38, 36, 33, # コブラ、スパイダー、バッタ、タートル、モグラ、
        37, 31, 84, 20, 24, # スコーピオン、クジャク、コンドル、ホワイトレオ、ケツァルコアトルス
        70                  # クリスマス
    ]
STAMP_INDEX_MIN =  0
STAMP_INDEX_MAX = len(STAMP_INDEX_ID_MAP) - 1

class StateManager():
    def __init__(self, sound_controller, leds_controller):
        self.__sound_controller = sound_controller
        self.__leds_controller  = leds_controller
        self.__prev_state = STATE_INIT
        self.__state = STATE_INIT
        self.__prev_stamp_id = 1
        self.__stamp_id = 1
        self.__stamp_index = 0
        self.__next_sign = SIGN_ZERO
        self.__timer_start_point_ms = 0
        self.__is_driver_light_on = False
        self.__state_change_point_ms = 0
        self.__command_from_stamp = COMMAND_NONE
        self.__state_change_lock = _thread.allocate_lock()
        self.__command_from_stamp_lock = _thread.allocate_lock()

    def get_state(self):
        return self.__state

    def get_stamp_id(self):
        return self.__stamp_id

    def get_timer_start_point_ms(self):
        return self.__timer_start_point_ms

    def get_is_driver_light_on(self):
        return self.__is_driver_light_on

    def get_state_change_point_ms(self):
        return self.__state_change_point_ms

    def get_command_from_stamp(self):
        return self.__command_from_stamp

    def state_change(self, new_state):
        self.__state_change_lock.acquire()
        self.__prev_state = self.__state
        self.__state = new_state
        self.__sound_controller.play(self.__prev_state, self.__state, self.__stamp_id)
        self.__leds_controller.set_main_color(self.__stamp_id)
        self.__leds_controller.change_pic(self.__prev_state, self.__state, self.__prev_stamp_id, self.__stamp_id)
        if self.__state == STATE_INIT:
            self.__state_change_point_ms = utime.ticks_ms()
        elif self.__state in [STATE_STAMP, STATE_CHANGE, STATE_FINISH, STATE_REMIX, STATE_WEAPON]:
            self.__timer_start_point_ms = utime.ticks_ms()
            self.__is_driver_light_on = True
        print("State: {} -> {}".format(STATE_STR[self.__prev_state], STATE_STR[self.__state]))
        self.__state_change_lock.release()

    def stamp_change(self, diff):
        if diff > 0:
            self.__next_sign = SIGN_PLUS
        elif diff < 0:
            self.__next_sign = SIGN_MINUS
        else:
            self.__next_sign = SIGN_ZERO

        self.__stamp_index += diff
        if self.__stamp_index < STAMP_INDEX_MIN:
            self.__stamp_index = STAMP_INDEX_MIN
            self.__next_sign = SIGN_ZERO
        elif self.__stamp_index > STAMP_INDEX_MAX:
            self.__stamp_index = STAMP_INDEX_MAX
            self.__next_sign = SIGN_ZERO

        self.__prev_stamp_id = self.__stamp_id
        self.__stamp_id = STAMP_INDEX_ID_MAP[self.__stamp_index]

    def set_is_driver_light_on(self, value):
        self.__is_driver_light_on = value

    def set_command_from_stamp(self, value):
        self.__command_from_stamp_lock.acquire()
        self.__command_from_stamp = value
        self.__command_from_stamp_lock.release()

    def control_leds(self):
        self.__leds_controller.control_leds(self.__prev_state, self.__state, self.__next_sign)

leds_controller.py

StateManagerがお知らせしてくる状態遷移に従って、LEDマトリクスをどのように光らせるかを管理する役割です。ここに、各生物のドット絵を表す配列(STAMP_PIC_1, STAMP_PIC_2)も記載しているのですが、全部掲載するとそれだけで600行近くになってしまうので、ここでは例を一つだけ載せて他は省略させて頂きます。何卒ご了承くださいませ。

import utime
from common import *
from my_ws2812 import myWS2812

NUM_LEDS = 160
FRAME_MAX = 10

BK = ( 0,  0,  0)
WH = (25, 25, 25)
RD = (25,  0,  0)
GR = ( 0, 25,  0)
BL = ( 0,  0, 25)
YL = (25, 25,  0)
OR = (24, 10,  0)
PP = (13,  4, 22)
PK = (25,  0, 25)
# 色を後から変える用のダミー変数
VR = (255,255,255)

# LEDの配置は以下の通り:
#  144 128 ... 16  0
#    |   | ...  |  |
#  147 131 ... 19  3
#  148 132 ... 20  4
#    |   | ...  |  |
#  155 139 ... 27 11
#  156 140 ... 28 12
#    |   | ...  |  |
#  159 143 ... 31 15
#           USB

EFFECT_SIDE_DURATION_MS   =  500
TO_SIDE_DURATION_MS       = 1000
ARROW_DURATION_MS         =   50
REMIX_DURATION_MS         =   30
APPEAL_MOTION_DURATION_MS =  400
APPEAL_EFFECT_DURATION_MS =   50

#################################################################
PIC_BLANK = [
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK
]

PIC_VICE = [
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BL,BL,BL,BL,BL,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BL,BL,BL,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BL,BL,BL,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BL,BL,BL,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BL,BL,BL,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BL,BL,BL,BL,BL,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK
]

PIC_GENOME = [
        RD,BK,BK,BK,BK,BK,BK,BL,BL,BK,BK,BK,BK,BK,BK,RD,
        RD,RD,BK,BK,BK,BK,BL,BL,BL,BL,BK,BK,BK,BK,RD,RD,
        RD,RD,RD,BK,BK,BL,BL,BL,BL,BL,BL,BK,BK,RD,RD,RD,
        BK,RD,RD,RD,BL,BL,BL,BK,BK,BL,BL,BL,RD,RD,RD,BK,
        BK,BK,RD,RD,RD,BL,BK,BK,BK,BK,BL,BL,BL,RD,BK,BK,
        BK,BK,BL,RD,RD,RD,BK,BK,BK,BK,RD,BL,BL,BL,BK,BK,
        BK,BL,BL,BL,RD,RD,RD,BK,BK,RD,RD,RD,BL,BL,BL,BK,
        BL,BL,BL,BK,BK,RD,RD,RD,RD,RD,RD,BK,BK,BL,BL,BL,
        BL,BL,BK,BK,BK,BK,RD,RD,RD,RD,BK,BK,BK,BK,BL,BL,
        BL,BK,BK,BK,BK,BK,BK,RD,RD,BK,BK,BK,BK,BK,BK,BL
]

STAMP_PIC_1 = {
    1: [
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,PK,PK,PK,BK,PK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,GR,PK,RD,PK,PK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,PK,PK,RD,PK,PK,BK,PK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,PK,RD,PP,PK,BK,PK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,PK,BK,PK,PK,PK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,PP,PK,BK,PK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,PP,BK,BK,PK,PK,PK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,PP,PK,PK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK
    ],

    ####################### 中略 #######################

    70: [
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,YL,YL,YL,BK,YL,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,RD,YL,RD,YL,YL,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,YL,YL,RD,YL,YL,BK,YL,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,YL,RD,OR,YL,BK,YL,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,YL,BK,YL,YL,YL,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,OR,YL,BK,YL,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,OR,BK,BK,YL,YL,YL,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,OR,YL,YL,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK
    ]
}

#################################################################

STAMP_PIC_2 = {
    1: [
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,PK,PK,PK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,GR,PK,RD,PK,PK,BK,PK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,PK,PK,RD,PK,PK,PK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,PK,RD,PP,PK,BK,BK,PK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,PK,BK,PK,PK,PK,BK,PK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,PP,PK,PK,PK,PK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,PP,PK,PK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,PP,PP,PK,PK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK
    ],

    ####################### 中略 #######################

    70: [
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,YL,YL,YL,BK,BK,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,RD,YL,RD,YL,YL,BK,YL,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,YL,YL,RD,YL,YL,YL,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,YL,RD,OR,YL,BK,BK,YL,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,YL,BK,YL,YL,YL,BK,YL,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,OR,YL,YL,YL,YL,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,OR,YL,YL,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,OR,OR,YL,YL,BK,BK,BK,BK,BK,BK,
        BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK
    ]
}

STAMP_MAIN_COLOR = {
     1: PK, 13: GR, 16: RD, 74: RD, 14: PK,
    23: YL,  6: YL, 11: OR,  9: BL,  4: PK,
    46: RD,  3: YL,  8: RD, 76: BL, 39: WH,
    40: WH, 41: RD, 38: GR, 36: RD, 33: GR,
    37: RD, 31: OR, 84: PP, 20: WH, 24: GR,
    70: RD
}

#################################################################

class LedsController():
    def __init__(self, pin_led):
        self.__disp_start_time_point_ms = 0
        self.__frame_num =  0
        self.__prev_pattern_start_elapsed_time_ms = 0

        self.__pic_1 = STAMP_PIC_1[1]
        self.__pic_2 = STAMP_PIC_2[1]

        self.__pic_arrow_down = [
                BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,VR,BK,BK,BK,BK,
                BK,BK,BK,VR,BK,BK,BK,BK,BK,BK,BK,VR,VR,BK,BK,BK,
                BK,BK,BK,VR,VR,VR,BK,BK,BK,BK,BK,VR,VR,VR,BK,BK,
                BK,BK,BK,VR,VR,VR,VR,BK,BK,BK,BK,VR,VR,VR,VR,BK,
                BK,BK,BK,VR,VR,VR,VR,VR,BK,BK,BK,VR,VR,VR,VR,VR,
                BK,BK,BK,VR,VR,VR,VR,VR,BK,BK,BK,VR,VR,VR,VR,VR,
                BK,BK,BK,VR,VR,VR,VR,BK,BK,BK,BK,VR,VR,VR,VR,BK,
                BK,BK,BK,VR,VR,VR,BK,BK,BK,BK,BK,VR,VR,VR,BK,BK,
                BK,BK,BK,VR,VR,BK,BK,BK,BK,BK,BK,VR,VR,BK,BK,BK,
                BK,BK,BK,VR,BK,BK,BK,BK,BK,BK,BK,VR,BK,BK,BK,BK
        ]

        self.__pic_arrow_left = [
                BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,VR,VR,VR,VR,VR,VR,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,BK,VR,VR,VR,VR,BK,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,BK,BK,VR,VR,BK,BK,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,VR,VR,VR,VR,VR,VR,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,BK,VR,VR,VR,VR,BK,BK,BK,BK,BK,BK,
                BK,BK,BK,BK,BK,BK,BK,VR,VR,BK,BK,BK,BK,BK,BK,BK
        ]

        self.__main_color = STAMP_MAIN_COLOR[1]

        self.__leds = myWS2812(pin_led, NUM_LEDS)
        for frame_num in range(10):
            self.__leds.to_advent_random(frame_num, PIC_VICE)
            self.__leds.pixels_show()
            utime.sleep_ms(20)

        utime.sleep(2)

        for frame_num in range(10):
            self.__leds.to_fade_out_random(frame_num, PIC_VICE)
            self.__leds.pixels_show()
            utime.sleep_ms(20)        

    def __pattern_init_check(self, patten_start_elapsed_time_ms):
        if self.__prev_pattern_start_elapsed_time_ms != patten_start_elapsed_time_ms:
            self.__prev_pattern_start_elapsed_time_ms = patten_start_elapsed_time_ms
            self.__frame_num = 0

    def __pattern_action(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms, num_action):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        duration_ms = total_animation_ms / num_action
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            if self.__frame_num % 2 == 0:
                self.__leds.set_pic(self.__pic_1)
            else:
                self.__leds.set_pic(self.__pic_2)
            self.__frame_num += 1

    def __pattern_to_left(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # 呼び出しは最低11回必要
        duration_ms = total_animation_ms / 12
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            self.__leds.to_left(self.__frame_num, self.__pic_1, self.__pic_2)
            self.__frame_num += 1

    def __pattern_to_right(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # 呼び出しは最低11回必要
        duration_ms = total_animation_ms / 12
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            self.__leds.to_right(self.__frame_num, self.__pic_1, self.__pic_2)
            self.__frame_num += 1

    def __pattern_to_down_loop(self, elapsed_time_ms, patten_start_elapsed_time_ms, inteval_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # frame_numを0〜15の範囲でループさせながら呼び出す
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // inteval_ms != self.__frame_num:
            self.__leds.to_down(self.__frame_num % 16, self.__pic_1, self.__pic_2)
            self.__frame_num += 1      

    def __pattern_to_left_loop(self, elapsed_time_ms, patten_start_elapsed_time_ms, inteval_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # frame_numを0〜9の範囲でループさせながら呼び出す
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // inteval_ms != self.__frame_num:
            self.__leds.to_left(self.__frame_num % 10, self.__pic_1, self.__pic_2)
            self.__frame_num += 1

    def __pattern_remix(self, elapsed_time_ms, patten_start_elapsed_time_ms, inteval_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # frame_numを0〜15の範囲でループさせながら呼び出す
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // inteval_ms != self.__frame_num:
            self.__leds.to_down(self.__frame_num % 16, PIC_GENOME, PIC_GENOME)
            self.__frame_num += 1

    def __pattern_effect_in(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # 呼び出しは最低6回必要
        duration_ms = total_animation_ms // 8
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            self.__leds.effect_side_in(self.__frame_num, PIC_BLANK, self.__main_color)
            self.__frame_num += 1

    def __pattern_effect_out(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # 呼び出しは最低6回必要
        duration_ms = total_animation_ms // 8
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            self.__leds.effect_side_out(self.__frame_num, PIC_BLANK, self.__main_color)
            self.__frame_num += 1

    def __pattern_advent_out(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # 呼び出しは最低6回必要
        duration_ms = total_animation_ms // 8
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            self.__leds.effect_side_out(self.__frame_num, self.__pic_1, self.__main_color)
            self.__frame_num += 1

    def __pattern_advent_down_out(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # 呼び出しは最低17回必要
        duration_ms = total_animation_ms // 17
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            self.__leds.effect_down_out(self.__frame_num, self.__pic_1, self.__main_color)
            self.__frame_num += 1

    def __pattern_advent_random(self, elapsed_time_ms, patten_start_elapsed_time_ms, total_animation_ms):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        # 呼び出しは最低10回必要
        duration_ms = total_animation_ms / 11
        if (elapsed_time_ms - patten_start_elapsed_time_ms) // duration_ms != self.__frame_num:
            self.__leds.to_advent_random(self.__frame_num, self.__pic_1)
            self.__frame_num += 1

    def __pattern_appeal(self, elapsed_time_ms, patten_start_elapsed_time_ms, motion_duration, effect_duration):
        self.__pattern_init_check(patten_start_elapsed_time_ms)
        inner_elapsed_time_ms = elapsed_time_ms - patten_start_elapsed_time_ms
        if inner_elapsed_time_ms // effect_duration != self.__frame_num:
            if (inner_elapsed_time_ms // motion_duration) % 2 == 0:
                self.__leds.effect_appeal_out(self.__frame_num % 5, self.__pic_1, self.__main_color)
            else:
                self.__leds.effect_appeal_out(self.__frame_num % 5, self.__pic_2, self.__main_color)
            self.__frame_num += 1

    def set_main_color(self, stamp_id):
        self.__main_color = STAMP_MAIN_COLOR[stamp_id]
        for i in range(0, NUM_LEDS):
            if self.__pic_arrow_down[i] != BK:
                self.__pic_arrow_down[i] = self.__main_color
            if self.__pic_arrow_left[i] != BK:
                self.__pic_arrow_left[i] = self.__main_color 

    def change_pic(self, prev_state, state, prev_stamp_id, stamp_id):
        if state == STATE_INIT:
            pass
        elif state == STATE_ACTIVATE:
            self.__pic_1 = STAMP_PIC_1[stamp_id]
            self.__pic_2 = STAMP_PIC_2[stamp_id]
        elif state == STATE_GENOME_CHANGE:
            self.__pic_1 = STAMP_PIC_1[prev_stamp_id]
            self.__pic_2 = STAMP_PIC_1[stamp_id]
        elif state == STATE_LECTURE:
            self.__pic_1 = STAMP_PIC_1[stamp_id]
            self.__pic_2 = STAMP_PIC_2[stamp_id]
        elif state == STATE_STAMP:
            self.__pic_1 = self.__pic_arrow_down
            self.__pic_2 = self.__pic_arrow_down
        elif state == STATE_SET:
            self.__pic_1 = self.__pic_arrow_left
            self.__pic_2 = self.__pic_arrow_left
        elif state == STATE_CHANGE:
            if prev_state != STATE_DRIVER_GENOME_CHANGE:
                self.__pic_1 = STAMP_PIC_1[stamp_id]
                self.__pic_2 = STAMP_PIC_2[stamp_id]
        elif state == STATE_DRIVER_GENOME_CHANGE:
            self.__pic_1 = STAMP_PIC_1[prev_stamp_id]
            self.__pic_2 = STAMP_PIC_1[stamp_id]
        elif state == STATE_FINISH_READY:
            self.__pic_1 = self.__pic_arrow_left
            self.__pic_2 = self.__pic_arrow_left
        elif state == STATE_FINISH:
            self.__pic_1 = STAMP_PIC_1[stamp_id]
            self.__pic_2 = STAMP_PIC_2[stamp_id]
        elif state == STATE_REMIX_READY:
            self.__pic_1 = self.__pic_arrow_left
            self.__pic_2 = self.__pic_arrow_left
        elif state == STATE_REMIX:
            self.__pic_1 = STAMP_PIC_1[stamp_id]
            self.__pic_2 = STAMP_PIC_2[stamp_id]
        elif state == STATE_WEAPON_READY:
            if prev_state != STATE_WEAPON_GENOME_CHANGE:
                self.__pic_1 = STAMP_PIC_1[stamp_id]
                self.__pic_2 = STAMP_PIC_2[stamp_id]
        elif state == STATE_WEAPON_GENOME_CHANGE:
            self.__pic_1 = STAMP_PIC_1[prev_stamp_id]
            self.__pic_2 = STAMP_PIC_1[stamp_id]
        elif state == STATE_WEAPON:
            self.__pic_1 = STAMP_PIC_1[stamp_id]
            self.__pic_2 = STAMP_PIC_2[stamp_id]
        elif state == STATE_WEAPON_CONFIRM:
            self.__pic_1 = STAMP_PIC_1[stamp_id]
            self.__pic_2 = STAMP_PIC_2[stamp_id]
        else:
            pass

        self.__frame_num = 0
        self.__disp_start_time_point_ms = utime.ticks_ms()

    def control_leds(self, prev_state, state, next_sign):
        # 表示データの切り替え自体はstate_changeで行う。ここは切り替え後の画像データのアニメーション表示を担う
        elapsed_time_ms = utime.ticks_ms() - self.__disp_start_time_point_ms

        if state == STATE_INIT:
            if prev_state in [STATE_ACTIVATE]:
                if elapsed_time_ms < 500:
                    self.__pattern_effect_in(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
                elif 500 <= elapsed_time_ms < 1000:
                    self.__pattern_advent_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
                elif 1000 <= elapsed_time_ms < 3000:
                    self.__pattern_action(elapsed_time_ms, 1000, 2000, 10)
                elif 3000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
            elif prev_state == STATE_GENOME_CHANGE:
                if elapsed_time_ms < 1000:
                    if next_sign == SIGN_PLUS:
                        self.__pattern_to_left(elapsed_time_ms, 0, TO_SIDE_DURATION_MS)
                    elif next_sign == SIGN_MINUS:
                        self.__pattern_to_right(elapsed_time_ms, 0, TO_SIDE_DURATION_MS)
                    else:
                        self.__leds.set_pic(self.__pic_1)
                elif 1000 <= elapsed_time_ms < 3000:
                    self.__leds.set_pic(self.__pic_2)
                elif 3000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
            elif prev_state == STATE_LECTURE:
                if elapsed_time_ms < 800:
                    self.__pattern_advent_random(elapsed_time_ms, 0, 800)
                elif 800 <= elapsed_time_ms < 5000:
                    self.__pattern_action(elapsed_time_ms, 800, 4200, 12)
                elif 5000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
            else:
                self.__leds.pixels_fill(BK)
        elif state == STATE_ACTIVATE:
            pass
        elif state == STATE_GENOME_CHANGE:
            pass
        elif state == STATE_LECTURE:
            pass
        elif state == STATE_STAMP:
            if prev_state == STATE_INIT:
                if elapsed_time_ms < 500:
                    self.__pattern_effect_out(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
                elif 500 <= elapsed_time_ms < 1000:
                    self.__pattern_effect_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
                elif 1000 <= elapsed_time_ms < 1500:
                    self.__pattern_effect_out(elapsed_time_ms, 1000, EFFECT_SIDE_DURATION_MS)
                elif 1500 <= elapsed_time_ms < 15000:
                    self.__pattern_to_down_loop(elapsed_time_ms, 1500, ARROW_DURATION_MS)
                elif 15000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
        elif state == STATE_SET:
                if elapsed_time_ms < 500:
                    self.__pattern_effect_out(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
                elif 500 <= elapsed_time_ms < 1000:
                    self.__pattern_effect_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
                elif 1000 <= elapsed_time_ms < 15000:
                    self.__pattern_to_left_loop(elapsed_time_ms, 1000, ARROW_DURATION_MS)
                elif 15000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
        elif state == STATE_CHANGE:
            if prev_state == STATE_DRIVER_GENOME_CHANGE:
                if elapsed_time_ms < 1000:
                    if next_sign == SIGN_PLUS:
                        self.__pattern_to_left(elapsed_time_ms, 0, TO_SIDE_DURATION_MS)
                    elif next_sign == SIGN_MINUS:
                        self.__pattern_to_right(elapsed_time_ms, 0, TO_SIDE_DURATION_MS)
                    else:
                        self.__leds.set_pic(self.__pic_1)
                elif 1000 <= elapsed_time_ms < 3000:
                    self.__leds.set_pic(self.__pic_2)
                elif 3000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
            elif prev_state == STATE_SET:
                if elapsed_time_ms < 500:
                    self.__pattern_effect_out(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
                elif 500 <= elapsed_time_ms < 1000:
                    self.__pattern_effect_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
                elif 1000 <= elapsed_time_ms < 1500:
                    self.__pattern_effect_out(elapsed_time_ms, 1000, EFFECT_SIDE_DURATION_MS)
                elif 1500 <= elapsed_time_ms < 2000:
                    self.__pattern_effect_out(elapsed_time_ms, 1500, EFFECT_SIDE_DURATION_MS)
                elif 2000 <= elapsed_time_ms < 2500:
                    self.__pattern_advent_out(elapsed_time_ms, 2000, EFFECT_SIDE_DURATION_MS)
                elif 2500 <= elapsed_time_ms < 16000:
                    self.__pattern_appeal(elapsed_time_ms, 2500, APPEAL_MOTION_DURATION_MS, APPEAL_EFFECT_DURATION_MS)
                elif 16000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
        elif state == STATE_DRIVER_GENOME_CHANGE:
            pass
        elif state == STATE_FINISH_READY:
            if prev_state in [STATE_CHANGE, STATE_FINISH, STATE_REMIX]:
                if elapsed_time_ms < 500:
                    self.__pattern_effect_out(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
                elif 500 <= elapsed_time_ms < 1000:
                    self.__pattern_effect_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
                elif 1000 <= elapsed_time_ms < 1500:
                    self.__pattern_effect_out(elapsed_time_ms, 1000, EFFECT_SIDE_DURATION_MS)
                elif 1500 <= elapsed_time_ms < 15000:
                    self.__pattern_to_left_loop(elapsed_time_ms, 1500, ARROW_DURATION_MS)
                elif 15000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
        elif state == STATE_FINISH:
            if elapsed_time_ms < 500:
                self.__pattern_effect_out(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
            elif 500 <= elapsed_time_ms < 1000:
                self.__pattern_effect_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
            elif 1000 <= elapsed_time_ms < 1500:
                self.__pattern_effect_out(elapsed_time_ms, 1000, EFFECT_SIDE_DURATION_MS)
            elif 1500 <= elapsed_time_ms < 2000:
                self.__pattern_advent_out(elapsed_time_ms, 1500, EFFECT_SIDE_DURATION_MS)
            elif 2000 <= elapsed_time_ms < 9000:
                self.__pattern_appeal(elapsed_time_ms, 2000, APPEAL_MOTION_DURATION_MS, APPEAL_EFFECT_DURATION_MS)
            elif 9000 <= elapsed_time_ms:
                self.__leds.pixels_fill(BK)
        elif state == STATE_REMIX_READY:
            if elapsed_time_ms < 500:
                self.__pattern_effect_in(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
            elif 500 <= elapsed_time_ms < 1700:
                self.__pattern_remix(elapsed_time_ms, 500, REMIX_DURATION_MS)
            elif 1700 <= elapsed_time_ms < 15000:
                self.__pattern_to_left_loop(elapsed_time_ms, 1700, ARROW_DURATION_MS)
            elif 15000 <= elapsed_time_ms:
                self.__leds.pixels_fill(BK)
        elif state == STATE_REMIX:
            if elapsed_time_ms < 500:
                self.__pattern_effect_out(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
            elif 500 <= elapsed_time_ms < 1000:
                self.__pattern_effect_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
            elif 1000 <= elapsed_time_ms < 1500:
                self.__pattern_effect_out(elapsed_time_ms, 1000, EFFECT_SIDE_DURATION_MS)
            elif 1500 <= elapsed_time_ms < 2000:
                self.__pattern_effect_out(elapsed_time_ms, 1500, EFFECT_SIDE_DURATION_MS)
            elif 2000 <= elapsed_time_ms < 2500:
                self.__pattern_advent_out(elapsed_time_ms, 2000, EFFECT_SIDE_DURATION_MS)
            elif 2500 <= elapsed_time_ms < 8000:
                self.__pattern_appeal(elapsed_time_ms, 2500, APPEAL_MOTION_DURATION_MS, APPEAL_EFFECT_DURATION_MS)
            elif 8000 <= elapsed_time_ms:
                self.__leds.pixels_fill(BK)
        elif state == STATE_WEAPON_READY:
            if prev_state == STATE_INIT:
                if elapsed_time_ms < 500:
                    self.__pattern_effect_out(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
                elif 500 <= elapsed_time_ms < 1000:
                    self.__pattern_effect_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
                elif 1000 <= elapsed_time_ms < 1500:
                    self.__pattern_advent_out(elapsed_time_ms, 1000, EFFECT_SIDE_DURATION_MS)
                elif 1500 <= elapsed_time_ms < 20000:
                    self.__pattern_action(elapsed_time_ms, 1200, 18800, 45)
                elif 20000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
            elif prev_state == STATE_WEAPON_GENOME_CHANGE:
                if elapsed_time_ms < 1000:
                    if next_sign == SIGN_PLUS:
                        self.__pattern_to_left(elapsed_time_ms, 0, TO_SIDE_DURATION_MS)
                    elif next_sign == SIGN_MINUS:
                        self.__pattern_to_right(elapsed_time_ms, 0, TO_SIDE_DURATION_MS)
                    else:
                        self.__leds.set_pic(self.__pic_1)
                elif 1000 <= elapsed_time_ms < 3000:
                    self.__leds.set_pic(self.__pic_2)
                elif 3000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
            elif prev_state == STATE_WEAPON:
                if elapsed_time_ms < 7500:
                    self.__pattern_appeal(elapsed_time_ms, 0, APPEAL_MOTION_DURATION_MS, APPEAL_EFFECT_DURATION_MS)
                elif 7500 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
            elif prev_state == STATE_WEAPON_CONFIRM:
                if elapsed_time_ms < 500:
                    self.__pattern_effect_in(elapsed_time_ms, 0, EFFECT_SIDE_DURATION_MS)
                elif 500 <= elapsed_time_ms < 1000:
                    self.__pattern_advent_out(elapsed_time_ms, 500, EFFECT_SIDE_DURATION_MS)
                elif 1000 <= elapsed_time_ms < 1500:
                    self.__leds.set_pic(self.__pic_1)
                elif 1500 <= elapsed_time_ms < 20000:
                    self.__pattern_action(elapsed_time_ms, 1500, 18500, 40)
                elif 20000 <= elapsed_time_ms:
                    self.__leds.pixels_fill(BK)
        elif state == STATE_WEAPON_GENOME_CHANGE:
            pass
        elif state == STATE_WEAPON:
            if elapsed_time_ms < 2000:
                self.__leds.set_pic(self.__pic_2)
            elif 2000 <= elapsed_time_ms:
                self.__leds.pixels_fill(BK)
        elif state == STATE_WEAPON_CONFIRM:
            pass
        else:
            pass

        self.__leds.pixels_show()

ws2812.py

先にも書きましたが、これは私が書いたプログラムではなく、こちらで公開されているものになります。WS2812(NeoPixel)を利用するための基本ライブラリになります。

my_ws2812.py

上記のws2812を継承して、ドット絵のアニメーション用のメソッドを色々追加しています。

from ws2812 import WS2812

class myWS2812(WS2812):
    def __init__(self, pin_num, led_count, brightness = 0.5):
        super().__init__(pin_num, led_count, brightness)
        # MicroPythonではランダム順の配列は生成しにくいので、
        # Python3系で print(random.sample(range(160), 160)) で作成したランダム配列を記載
        self.__RANDOM_INDEX = [
            41, 125,  37, 100, 101,  34, 130,  55, 107,  62,  23,  51, 121,  75,  91,  15,
           151, 149,   9, 145,  39, 135, 109,  46,   4,  18,  25, 159,  95,  53,  99,  43,
            78,  49,  97, 119,  35,  40,  74,  19,  47, 129, 153,  44, 106, 128,  45,  94,
           113, 158, 104,  93,  10,  20, 150,  64,  73, 152, 138,  56, 103, 112,  89,  36,
           143,  13,  96,  17, 132,  60, 141, 116,  52, 127,  50,  77, 114,  33, 124, 156,
            16, 111,  12, 139,   5,  92,  58, 108,   8,  57, 117, 126,  30, 134, 148,   6,
           122,  42, 136, 154,   2, 144,  72,  87,  22,  68, 102,  84,  14, 133, 118,  80,
            67,  71,  81,  21,  83, 120,  59,  28,  65, 123,  29,  11,   1,  70, 110,  69,
           131,  38, 142, 137,  63,  31, 146,  82,  86,   3,  48, 140,  90,  54,  88,  32,
           155,  98,   0, 105,  61,  79,   7, 147, 115,  26,  85,  27,  76, 157,  66,  24
        ]
        self.__random_pic = [(0,0,0)] * 160

    def set_pic(self, pic_data):
        for i in range(len(self.ar)):
            self.pixels_set(i, pic_data[i])

    def to_left(self, col_num, old_pic, new_pic):
        if col_num < 0:
            col_num = 0
        elif col_num > 10:
            col_num = 10

        to_left_data = [x for x in old_pic]

        # 新しいデータの左側を入れる
        for i in range(16*(10-col_num), 160):
            to_left_data[i - 16*(10-col_num)] = new_pic[i]

        # 古いデータの右側を入れる
        for i in range(0, 160-16*col_num):
            to_left_data[16*col_num + i] = old_pic[i]

        for i in range(len(to_left_data)):
            self.pixels_set(i, to_left_data[i])

    def to_right(self, col_num, old_pic, new_pic):
        if col_num < 0:
            col_num = 0
        elif col_num > 10:
            col_num = 10

        to_right_data = [x for x in old_pic]

        # 古いデータの左側を入れる
        for i in range(16*col_num, 160):
            to_right_data[i - 16*col_num] = old_pic[i]

        # 新しいデータの右側を入れる
        for i in range(0, 16*col_num):
            to_right_data[16*(10-col_num) + i] = new_pic[i]        

        for i in range(len(to_right_data)):
            self.pixels_set(i, to_right_data[i])

    def to_down(self, row_num, old_pic, new_pic):
        if row_num < 0:
            row_num = 0
        elif row_num > 16:
            row_num = 16

        to_down_data = [x for x in old_pic]

        # 新しいデータの下側を入れる
        for i in range(0, row_num):
            for j in range(0, 10):
                to_down_data[i+j*16] = new_pic[(16-(row_num-i))+j*16]

        # 古いデータの上側を入れる
        for i in range(row_num, 16):
            for j in range(0,10):
                to_down_data[i+j*16] = old_pic[(i-row_num)+j*16]

        for i in range(len(to_down_data)):
            self.pixels_set(i, to_down_data[i])

    def effect_side_in(self, frame_num, pic, color):
        if frame_num < 0:
            frame_num = 0
        elif frame_num > 5:
            frame_num = 5

        effect_data = [x for x in pic]

        # 出現ライン上の色をセット
        start_index = frame_num*16
        for j in range(start_index, start_index+16):
            effect_data[j] = color
        start_index = (9-frame_num)*16
        for j in range(start_index, start_index+16):
            effect_data[j] = color            

        # 出現ライン外は消灯
        for i in range(0, frame_num*16):
            effect_data[i] = (0,0,0)
        for i in range((10-frame_num)*16+1, 160):
            effect_data[i] = (0,0,0)

        for i in range(len(effect_data)):
            self.pixels_set(i, effect_data[i])

    def effect_side_out(self, frame_num, pic, color):
        if frame_num < 0:
            frame_num = 0
        elif frame_num > 5:
            frame_num = 5

        effect_data = [x for x in pic]

        if frame_num < 5:
            # 出現ライン上の色をセット
            start_index = (4-frame_num)*16
            for j in range(start_index, start_index+16):
                effect_data[j] = color
            start_index = (5+frame_num)*16
            for j in range(start_index, start_index+16):
                effect_data[j] = color            

            # 出現ライン外は消灯
            for i in range(0, (4-frame_num)*16):
                effect_data[i] = (0,0,0)
            for i in range((6+frame_num)*16, 160):
                effect_data[i] = (0,0,0)        

        for i in range(len(effect_data)):
            self.pixels_set(i, effect_data[i])

    def effect_down_out(self, frame_num, pic, color):
        if frame_num < 0:
            frame_num = 0
        elif frame_num > 16:
            frame_num = 16

        effect_data = [x for x in pic]

        if frame_num < 16:
            # 出現ラインの色をセット
            for i in range(0, 10):
                effect_data[frame_num+i*16] = color

            # 出現ラインより下は消灯
            for i in range(0, 10):
                for j in range(frame_num+1+i*16, (i+1)*16):
                    effect_data[j] = (0,0,0)

        for i in range(len(effect_data)):
            self.pixels_set(i, effect_data[i])

    def effect_appeal_out(self, frame_num, pic, color):
        if frame_num < 0:
            frame_num = 0
        elif frame_num > 4:
            frame_num = 4

        effect_data = [x for x in pic]

        if frame_num < 4:
            for i in range(3-frame_num, 160, 16):
                effect_data[i]  = color

            for i in range(frame_num+12, 160, 16):
                effect_data[i]  = color

        for i in range(len(effect_data)):
            self.pixels_set(i, effect_data[i])

    def to_advent_random(self, frame_num, pic):
        if frame_num < 0:
            frame_num = 0
        elif frame_num > 9:
            frame_num = 9

        if frame_num == 0:
            self.__random_pic = [(0,0,0)] * 160

        for i in self.__RANDOM_INDEX[frame_num*16 : (frame_num+1)*16]:
            self.__random_pic[i] = pic[i]

        for i in range(len(self.__random_pic)):
            self.pixels_set(i, self.__random_pic[i])

    def to_fade_out_random(self, frame_num, pic):
        if frame_num < 0:
            frame_num = 0
        elif frame_num > 9:
            frame_num = 9

        if frame_num == 0:
            self.__random_pic = [x for x in pic]

        for i in self.__RANDOM_INDEX[frame_num*16 : (frame_num+1)*16]:
            self.__random_pic[i] = (0,0,0)

        for i in range(len(self.__random_pic)):
            self.pixels_set(i, self.__random_pic[i])

sound_controller.py

StateManagerがお知らせしてくる状態遷移に従って、どのように音を鳴らすかを管理する役割です。

from common import *
from dfplayer import DFPlayer

FOLDER_COMMON             =  1
FOLDER_ACTIVATE           =  2
FOLDER_LECTURE_1          =  3
FOLDER_LECTURE_2          =  4
FOLDER_STAMP_CHANGE_READY =  5
FOLDER_SET_CHANGE_READY   =  6
FOLDER_CHANGE             =  7
FOLDER_FINISH             =  8
FOLDER_REMIX              =  9
FOLDER_WEAPON             = 10

# COMMONフォルダのサウンド
SOUND_STAMP = 1
SOUND_SET   = 2
SOUND_EJECT = 3
SOUND_FINISH_READY = 4
SOUND_REMIX_READY = 5
SOUND_LETS_CHANGE = 6

class SoundController():
    def __init__(self, pin_tx, pin_rx, volume):
        self.__mp3_player = DFPlayer(pin_tx, pin_rx)
        self.__mp3_player.init_sd()
        self.__mp3_player.set_volume(volume)
        self.__is_lecture_1 = True
        self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_LETS_CHANGE)
        #print("MP3 Ready.")

    def play(self, prev_state, state, stamp_id):
        if prev_state == STATE_INIT:
            if state == STATE_ACTIVATE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
            elif state == STATE_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_LECTURE:
                if self.__is_lecture_1 == True:
                    self.__mp3_player.play_sound(FOLDER_LECTURE_1, stamp_id)
                else:
                    self.__mp3_player.play_sound(FOLDER_LECTURE_2, stamp_id)
                self.__is_lecture_1 = not self.__is_lecture_1
            elif state == STATE_STAMP:
                self.__mp3_player.play_sound(FOLDER_STAMP_CHANGE_READY, stamp_id)
            elif state == STATE_SET:
                self.__mp3_player.play_sound(FOLDER_SET_CHANGE_READY, stamp_id)
            elif state == STATE_WEAPON_READY:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_SET)
            elif state == STATE_WEAPON:
                self.__mp3_player.play_sound(FOLDER_WEAPON, stamp_id)
            else:
                pass
        elif prev_state == STATE_ACTIVATE:
            pass
        elif prev_state == STATE_GENOME_CHANGE:
            pass
        elif prev_state == STATE_LECTURE:
            pass
        elif prev_state == STATE_STAMP:
            if state == STATE_ACTIVATE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
            elif state == STATE_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_STAMP:
                self.__mp3_player.play_sound(FOLDER_STAMP_CHANGE_READY, stamp_id)
            elif state == STATE_SET:
                self.__mp3_player.play_sound(FOLDER_SET_CHANGE_READY, stamp_id)
            else:
                pass
        elif prev_state == STATE_SET:
            if state == STATE_INIT:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_EJECT)
            elif state == STATE_CHANGE:
                self.__mp3_player.play_sound(FOLDER_CHANGE, stamp_id)
            else:
                pass
        elif prev_state == STATE_CHANGE:
            if state == STATE_INIT:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_EJECT)
            elif state == STATE_DRIVER_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_FINISH_READY:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_FINISH_READY)
        elif prev_state == STATE_DRIVER_GENOME_CHANGE:
            pass
        elif prev_state == STATE_FINISH_READY:
            if state == STATE_INIT:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_EJECT)
            elif state == STATE_DRIVER_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_FINISH:
                self.__mp3_player.play_sound(FOLDER_FINISH, stamp_id)
            elif state == STATE_REMIX_READY:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_REMIX_READY)
            pass
        elif prev_state == STATE_FINISH:
            if state == STATE_INIT:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_EJECT)
            elif state == STATE_DRIVER_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_FINISH_READY:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_FINISH_READY)
        elif prev_state == STATE_REMIX_READY:
            if state == STATE_INIT:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_EJECT)
            elif state == STATE_DRIVER_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_REMIX:
                self.__mp3_player.play_sound(FOLDER_REMIX, stamp_id)
        elif prev_state == STATE_REMIX:
            if state == STATE_INIT:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_EJECT)
            elif state == STATE_DRIVER_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_FINISH_READY:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_FINISH_READY)
        elif prev_state == STATE_WEAPON_READY:
            if state == STATE_INIT:
                self.__mp3_player.play_sound(FOLDER_COMMON, SOUND_EJECT)
            elif state == STATE_WEAPON_GENOME_CHANGE:
                self.__mp3_player.play_sound(FOLDER_ACTIVATE, stamp_id)
                self.__is_lecture_1 = True
            elif state == STATE_WEAPON:
                self.__mp3_player.play_sound(FOLDER_WEAPON, stamp_id)
        elif prev_state == STATE_WEAPON_GENOME_CHANGE:
            pass
        elif prev_state == STATE_WEAPON:
            pass
        elif prev_state == STATE_WEAPON_CONFIRM:
            pass
        else:
            pass

dfplayer.py

DFPlayerを利用するためのライブラリ的役割です。前にも書いたのですが、今回はそれに「フォルダ分けされた音声ファイルを再生する」という機能を追加しています。

from machine import UART, Pin
import utime

class DFPlayer():
    def __init__(self, pin_tx, pin_rx):
        self.uart = UART(1, baudrate=9600, tx=Pin(pin_tx), rx=Pin(pin_rx))

    def __calc_checksum(self, sum_data):
        temp = ~sum_data + 1 # 2の補数の計算(ビットを反転させて1を足す)
        h_byte = (temp & 0xFF00) >> 8
        l_byte = temp & 0x00FF
        return h_byte, l_byte

    def __send_data(self, command, param):
        ver      = 0xFF
        d_len    = 0x06
        feedback = 0x00
        param1  = (param & 0xFF00) >> 8
        param2  = param & 0x00FF
        cs1, cs2 = self.__calc_checksum(ver + d_len + command + feedback + param1 + param2)
        sdata = bytearray([0x7E, ver, d_len, command, feedback, param1, param2, cs1, cs2, 0xEF])
        self.uart.write(sdata)

    def init_sd(self):
        self.__send_data(0x3F, 0x02) # 0x02でSDカードのみ有効化
        utime.sleep_ms(1000)

    def set_volume(self, volume):
        self.__send_data(0x06, volume)
        utime.sleep_ms(500)

    def play_sound(self, folder, num):
        # 01〜99フォルダ内の、001〜255.mp3ファイルを再生
        print("Play {} in folder {}".format(num, folder))
        self.__send_data(0x0F, (folder << 8) | num)
        #utime.sleep_ms(500)

以上になります。おつかれさまでした。