Tanuki_Bayashin’s diary

電子工作を趣味としています。最近はラズベリーパイPicoというマイコンを使って楽しんでいます

Xmas ツリーイルミネーション~コード編

※なにか気になる点がありましたらコメント欄からお伝えください。

目次

1.はじめに

 もりしーさん(@kotaMorishita :twitter)の懸賞でXmas tree のプリント基板が当たりました。今回はその基板にLEDをはんだ付けしたものを、ラズベリーパイ3にて制御したので、そのときのコードを張り付けておくことにしました。

 他人の書いたコード、もしくは自分のコードでも日数のおいたものは、難解を極めるとのことなので、あえて詳しい解説は少なめにしました。パズルの解読が好きな方には、リアルのなぞ解きに挑戦するつもりで、読んでいただくのも楽しいかもしれないです。

(単に詳しく解説するのが面倒なことと、それでもコードをそれなりに公開したい気持ちがあることから、このような形を取ったまでです。平にご容赦願います)

動画1 Xmas ツリー
youtu.be

参考にしたコード:もりしーさんによるXmas ツリー用のコード
         (oled.py のクラスは全面的にもりしーさんによるものです)
github.com

2.イルミネーションのコード

 以下にコードの中身のあらましを記述します。

  • 前処理
  • メインとなる処理 ステージ0からステージ8まである(動作は動画と下のコード内のコメントを見てください)
  • 締めくくり
  • 後処理


リスト1 Xmas ツリー用のコード

import RPi.GPIO as GPIO
import time
import random
from luma.core.render import canvas

# 以下の1行はもりしーさんがコーディングしたモジュールをインポートしています
# (今回のプログラムのため、私が少し手を加えました)
from oled import oled

# LEDが接続している端子(GPIO)
leds = [6, 5, 13, 11, 16, 19, 9, 12, 26]

# 奇数の段のLED
ledOdds = [6, 13, 16, 9, 26]
# 偶数の段のLED
ledEven = [5, 11, 19, 12]

# 回転して表示するときに使う
rotate = [6, 13, 19, 26, 12, 9, 16, 11, 5]

# 複数の段をオンオフするときに使う
SW_OnOff = [[1, 0, 0, 0],
            [1, 1, 0, 0],
            [1, 1, 1, 0]]

# もりしーさんのコードの名残
# LEDを段ごとに表示する
# You can reverse the array in the program too!
ledGroup = [[6], # top
          [5, 13], # 2nd
          [11, 16, 19], # 3rd
          [9, 12, 26]] # 4th

# 列ごとに表示するとき用(下から上)
ledColumUp = [[9, 11, 5, 6],
            [12, 16, 6],
            [26, 19, 13, 6]]

# 列ごとに表示するとき用(上から下)
ledColumDown = [[6, 5, 11, 9],
                 [6, 16, 12],
                 [6, 13, 19, 26]]

# LEDをランダムに表示するとき用
ledRandom = [0, 0, 0, 0, 0]

# GPIOの設定
GPIO.setmode(GPIO.BCM) 

# LEDを表示させるためのインスタンスを生成
for pins in ledGroup:
    for pin in pins:
        GPIO.setup(pin, GPIO.OUT)
        GPIO.output(pin, GPIO.LOW)

# OLED表示に使うインスタンスを生成
# (もりしーさんオリジナル)
monitor = oled.ssd1306_oled()
device ,font32 = monitor.setup()

# OLEDに”BEGIN”と表示
# (うまい表示のさせ方が分からず、
# 表示するごとにcanvas(device)を呼び出している)
with canvas(device) as drawUpdate:
    drawUpdate.text((5, 0), "BEGIN" , font=font32, fill=100)
# 0.8秒待つ(随所で使用している)
time.sleep(0.8)

try:
# STAGE0と表示(ステージ0開始)
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE0" , font=font32, fill=100)
    time.sleep(0.5)

# ステージを切り替えるための変数
    stage = 0

# ステージ0のループ
    while(stage == 0):
    # すべてのLED、3回点滅
        for _ in range(3):
            for pin in rotate:
                GPIO.output(pin, GPIO.HIGH)
            time.sleep(0.5)
            for pin in rotate:
                GPIO.output(pin, GPIO.LOW)
            time.sleep(0.5)
            
# 半分ずつ点滅
        for _ in range(3):
            for pin in ledOdds:
                GPIO.output(pin, GPIO.HIGH)
            time.sleep(0.5)
            for pin in ledOdds:
                GPIO.output(pin, GPIO.LOW)
            time.sleep(0.01)
            for pin in ledEven:
                GPIO.output(pin, GPIO.HIGH)
            time.sleep(0.5)
            for pin in ledEven:
                GPIO.output(pin, GPIO.LOW)
            time.sleep(0.01)

        stage = 1
    tm = 180.0
    count = 0

# ステージ1と表示(ステージ1開始)
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE1" , font=font32, fill=100)
    time.sleep(0.5)
    
    while(stage == 1):

# tmは待ち時間。tmを用いて回転の速さを調節
# 3周期でステージ2へ
        if tm <= 1.0:
            tm = 25.0
            count += 1
            if count >= 3:
                stage = 2
                j = 0
                tm = 100.0
                count = 0
                row = 0

# LEDを回転して表示
        for pin in rotate:
            GPIO.output(pin, GPIO.HIGH)
            time.sleep(tm/1000.0)
            GPIO.output(pin, GPIO.LOW)
            time.sleep(tm/1000.0)
            
        tm -= 0.5 * tm

# ステージ2開始(OLEDに表示)
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE2" , font=font32, fill=100)
    time.sleep(0.5)
    
# tmで速さを調節
# jで表示させる段の先頭の位置を決定
# rowで段の数を変化させている
# 2次元リストSW_OnOff で小細工

    while(stage == 2):
        if j >= 4:
            j = 0

        if tm <= 1.0:
            tm = 50.0
        
        if count >=50:
            row += 1
            count = 0

# メインの処理
        for i in range(4):
            if SW_OnOff[row][ (j - i) % 4 ]:
                for pin in ledGroup[i]:
                    GPIO.output(pin, GPIO.HIGH)
                time.sleep(tm / 1000.0)
            else:
                for pin in ledGroup[i]:
                    GPIO.output(pin, GPIO.LOW)
                time.sleep(tm / 1000.0)
        j += 1
        tm -= 1.0
        count += 1
        
        if row >= 2:
            stage = 3
            tm = 80.0
            count = 0

# すべてのLEDをオフ
    for i in leds:
        GPIO.output(i, GPIO.LOW)

# ステージ3突入
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE3" , font=font32, fill=100)
    time.sleep(0.5)
    
# 列ごとに上昇しながらLEDオン
    while(stage == 3):
        for i in range(3):
            for pin in ledColumUp[i]:
                GPIO.output(pin, GPIO.HIGH)
                time.sleep(tm / 1000.0)
            for pin in ledColumUp[i]:
                GPIO.output(pin, GPIO.LOW)
                time.sleep(tm / 1000.0)

# 周期の調整
        tm = 0.8 * tm
        if tm <= 0.1:
            stage = 4
            tm = 80.0

# ステージ4突入
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE4" , font=font32, fill=100)
    time.sleep(0.5)
    
# 列ごとに下降しながらLEDオン
    while(stage == 4):
        for i in range(3):
            for pin in ledColumDown[i]:
                GPIO.output(pin, GPIO.HIGH)
                time.sleep(tm / 1000.0)
            for pin in ledColumDown[i]:
                GPIO.output(pin, GPIO.LOW)
                time.sleep(tm / 1000.0)

        tm = 0.6 * tm
        if tm <= 0.1:
            stage = 5
            tm = 80.0

# ステージ5突入
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE5" , font=font32, fill=100)
    time.sleep(0.5)


#  乱数を導入。列ごとに乱数の段数まで点滅
    while(stage == 5):
        for i in range(3):
            n = random.randint(0, 2) + 1  # 1, 2, 3
            if i == 1:
                n = random.randint(0, 1) + 1 # 1, 2
            for j in range(n+1):
                GPIO.output(ledColumUp[i][j], GPIO.HIGH)
                time.sleep(tm / 1000.0)
            for j in range(n+1):
                GPIO.output(ledColumUp[i][j], GPIO.LOW)
                time.sleep(tm / 1000.0)

        tm -= 5
        if tm <= 30:
            stage = 6
            tm = 80.0

# ステージ6突入
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE6" , font=font32, fill=100)
    time.sleep(0.5)

# メイン処理。ランダムにLEDを5個表示させている
    while(stage == 6):
        for i in range(len(ledRandom)):
            ledRandom[i] = random.randint(0, 8)

        for i in ledRandom:
            GPIO.output(leds[i], GPIO.HIGH)
        time.sleep(tm / 1000.0)
        for i in ledRandom:
            GPIO.output(leds[i], GPIO.LOW)
        time.sleep(tm / 1000.0)

        tm -= 2
        if tm <= 30:
            stage = 7

# ステージ7ではあるが、LEDの表示に切れ目が出てしまうので、
# OLEDはステージ6のまま
#    with canvas(device) as drawUpdate:
#        drawUpdate.text((5, 0), "STAGE7" , font=font32, fill=100)
#    time.sleep(0.5)

# メイン処理
    while(stage == 7):
        for i in range(len(ledRandom)):
            ledRandom[i] = random.randint(0, 8)

        for i in ledRandom:
            GPIO.output(leds[i], GPIO.HIGH)
        time.sleep(tm / 1000.0)
        for i in ledRandom:
            GPIO.output(leds[i], GPIO.LOW)
        time.sleep(tm / 1000.0)

# テンポを遅らせている
        tm += 10
        if tm >= 150:
            stage = 8

    for i in ledRandom:
        GPIO.output(leds[i], GPIO.HIGH)
    time.sleep(tm / 1000.0)

# ステージ8だが表示は7(別にどっちでもいい)
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "STAGE7" , font=font32, fill=100)
    time.sleep(0.5)

# メイン処理(ステージ0と同じ)
# すべてのLEDを点滅
    while(stage == 8):
        for _ in range(3):
            for pin in leds:
                GPIO.output(pin, GPIO.HIGH)
            time.sleep(0.5)
            for pin in leds:
                GPIO.output(pin, GPIO.LOW)
            time.sleep(0.5)
            
# 半分ずつ点滅
        for _ in range(3):
            for pin in ledOdds:
                GPIO.output(pin, GPIO.HIGH)
            time.sleep(0.5)
            for pin in ledOdds:
                GPIO.output(pin, GPIO.LOW)
            time.sleep(0.01)
            for pin in ledEven:
                GPIO.output(pin, GPIO.HIGH)
            time.sleep(0.5)
            for pin in ledEven:
                GPIO.output(pin, GPIO.LOW)
            time.sleep(0.01)
        stage = 9

# 締めくくりに突入
    for pin in leds:
        GPIO.output(pin, GPIO.HIGH)
    time.sleep(0.5)

# 感謝の意を表す
    for _ in range(3):
        with canvas(device) as drawUpdate:
            drawUpdate.text((5, 0), "THANK" , font=font32, fill=100)
        time.sleep(0.4)
        with canvas(device) as drawUpdate:
            drawUpdate.text((5, 0), "YOU!!" , font=font32, fill=100)
        time.sleep(0.4)

# 後処理(フィナーレ)
finally:
    GPIO.cleanup()

    for _ in range(2):
        with canvas(device) as drawUpdate:
            drawUpdate.text((5, 0), "GOOD" , font=font32, fill=100)
        time.sleep(0.8)
        with canvas(device) as drawUpdate:
            drawUpdate.text((5, 0), "BYE !" , font=font32, fill=100)
        time.sleep(0.8)

# 変化を付けている(遊んでいる)
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "GOOD" , font=font32, fill=100)
    time.sleep(0.8)
    with canvas(device) as drawUpdate:
        drawUpdate.text((5, 0), "LUCK!!" , font=font32, fill=100)
    time.sleep(2)

#OLEDの表示を消している
    with canvas(device) as drawUpdate:
        time.sleep(0.1)

3.もりしーさんのモジュール(oled.py)

 OLEDを制御するコードです。もりしーさんのコードを使用させていただきました。出展は「1.はじめに」のリンク先を参照してください。

 メインのファイルから、呼び出すことができるように、自分用に少し手を加えました。(28~31行、35,6行をコメントアウトし、33行を付け加えている)

スト2 oled.py の中身

# -*- coding: utf-8 -*-
# sudo -H pip3 install --upgrade luma.oled
# sudo -H pip3 install serial
# 128 x 64
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import Image, ImageFont, ImageDraw, ImageOps
import time
import os

class ssd1306_oled(object):
    def __init__(self):
        print("oled initialize")

    def setup(self, i2c_address=0x3C):
        self.serial = i2c(port=1, address=i2c_address)
        self.device = ssd1306(self.serial)

        # font configuration
        self.ttf = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
        basedir = os.path.dirname(os.path.realpath(__file__))
        self.imagedir = os.path.join(basedir, 'images')
        self.font8 = ImageFont.truetype(self.ttf, 8)
        self.font16 = ImageFont.truetype(self.ttf, 16)
        self.font32 = ImageFont.truetype(self.ttf, 32)

#        with canvas(self.device) as drawUpdate:
#            drawUpdate.text((5, 0), "Hello" , font=self.font32, fill=100)

#        time.sleep(10)
        
        return self.device,  self.font32

# monitor = ssd1306_oled()
# monitor.setup()

4.まとめ

 もりしーさんの懸賞でXmas ツリーが当選したので、それのLEDを点滅させるプログラムの解説を行いました。

  • 繰り返しの処理があまりなかったので、関数にまとめることなく、延々と書き連ねていくスタイルを取りました。(読みづらかったかと思います)
  • OLEDの表示では、1度表示した文字を消すのに、何度も canvas() という関数を呼び出しました。非推奨です。

 気分としては、スクラッチなどで小学生ぐらいの子供がが、アニメーションをコーディングしているのとあまり変わらないのかな、といった印象を抱きました。思いつくままに、つけ足していって、少しくどい演出だったかもしれないです。
 しかし、これで、懸賞に当たったときからの宿題にこたえることができた、といった思いです。
 改めまして、もりしーさんには感謝の意を表したいと思います。ありがとうございました。