工作と競馬2

電子工作、プログラミング、木工といった工作の記録記事、競馬に関する考察記事を掲載するブログ

ラズパイオーディオの筐体の製作(3) ~ 操作プログラムの製作、完成 ~

概要

ラズパイオーディオ筐体の操作プログラムを作成し、完成させた。




背景と目的

前回、筐体の組み立てを行った。今回は、ボリュームなどの操作プログラムを作成し、完成させる。



詳細

0. 準備

今回作成する操作系は、

  • ①ロータリーエンコーダによるボリューム
  • 3つのタクトスイッチによる再生停止、トラック戻し、トラック送り

だ。①については、JustBoom Amp HAT基板のドキュメント

github.com

を見ると、ロータリーエンコーダを読み取るプログラムが用意されていたようなのだが、残念ながらすでに利用不可能になっている。そのため、自前で用意する。

GPIOを読み取る必要があるので、

sudo apt-get install python3-rpi.gpio

としてPythonライブラリをインストールしておく。


1. volume操作部

ロータリーエンコーダの操作を検出処理を一から作成するのは面倒なのでネット上で使えるものを探す。

zenn.dev

上記サイトを参考に

  • Rpi.GPIOを使う
  • 読み取りを周期的に行うようにスレッド化する
  • 読み取り時に任意の処理を呼び出すためのコールバックの仕組みを入れる
  • チャタリング対策

といった修正を行った。

import RPi.GPIO as GPIO
import sys
import time
import threading
import subprocess
import traceback

GPIO.setmode(GPIO.BCM)

class RotaryEncoder(threading.Thread):
    """
    ロータリーエンコーダ
    """

    def __init__(self, pin_a, pin_b, on_change=None):
        super().__init__(target=self.run, daemon=True)
        self.pin_a = pin_a
        self.pin_b = pin_b
        self.position = 0
        self.on_change = on_change
        self._stop_request = False
        self.min_interval = 0.1
        self.last_time = time.time()

        GPIO.setup(self.pin_a, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(self.pin_b, GPIO.IN, pull_up_down=GPIO.PUD_UP)

        self.last_pin_a = GPIO.input(self.pin_a)

    def run(self):
        try:
            while True:
                self.read()
                time.sleep(0.01)
                if self._stop_request:
                    print("Rotary encoder thread recieved stop request")
                    self._stop_request = False
                    break
        except Exception:
            print(traceback.format_exc())
            print("Error in rotary encoder thread")
        finally:
            GPIO.cleanup()

        print("Rotary encoder thread terminated")

    def read(self):
        current_pin_a = GPIO.input(self.pin_a)
        current_pin_b = GPIO.input(self.pin_b)
        direction = None
        if current_pin_a != self.last_pin_a:
            if current_pin_b != current_pin_a:
                position = self.position + 1
                direction = "CW"
            else:
                position = self.position - 1
                direction = "CCW"
            if (
                self.on_change is not None
                and direction != self.last_direction # チャタリング対策
                and time.time() - self.last_time > self.min_interval # チャタリング対策
            ):
                self.on_change[0](direction, self.position, self.on_change[1]) # 任意処理コールバック
                self.last_time = time.time()
                self.position = position
        self.last_pin_a = current_pin_a
        self.last_direction = direction
        return (direction, self.position)

    def stop(self):
        """
        終了
        """
        self._stop_request = True


2. 再生/停止、トラック操作部

こちらも、上記とほぼ同様の構成でクラス化。冒頭のインポート部は共通なので省略。

class SwitchBorad(threading.Thread):
    """
    スイッチ基板を読み取る
    """

    def __init__(
        self,
        prev_pin,
        play_pin,
        next_pin,
        on_next_pushed=None,
        on_play_pushed=None,
        on_prev_pushed=None,
    ):
        super().__init__(target=self.run, daemon=True)
        self.prev_pin = prev_pin
        self.play_pin = play_pin
        self.next_pin = next_pin

        GPIO.setup(self.prev_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(self.play_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(self.next_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

        self.on_play_pushed = on_play_pushed
        self.on_next_pushed = on_next_pushed
        self.on_prev_pushed = on_prev_pushed

    def run(self):
        try:
            while True:
                self.read()
                time.sleep(0.01)
        except Exception:
            print(traceback.format_exc())
            print("Error in switch borad thread")
        finally:
            GPIO.cleanup()

        print("Switch borad thread terminated")

    def read(self):
        """
        読み取る
        """
        self.read_basic(self.prev_pin, self.on_prev_pushed)
        self.read_basic(self.play_pin, self.on_play_pushed)
        self.read_basic(self.next_pin, self.on_next_pushed)

    def read_basic(self, target_pin, on_pushed=None):
        if GPIO.input(target_pin) == 0:
            while True:
                if GPIO.input(target_pin) == 1:
                    time.sleep(0.01)
                    break
            if on_pushed is not None:
                on_pushed[0](on_pushed[1])


3. スクリプト全体

上記2クラスを用いて、読み取り処理スクリプトを作成。

volumioの操作は、Command-Line Clientを使用した。

developers.volumio.com

def on_play_pushed(user_data):
    print("Play pushed")
    subprocess.run("volumio toggle", shell=True)


def on_next_pushed(user_data):
    print("Next pushed")
    subprocess.run("volumio next", shell=True)


def on_prev_pushed(user_data):
    print("Prev pushed")
    subprocess.run("volumio previous", shell=True)


def on_volume_change(direction, position, user_data):
    print(f"on_volume_change Direction: {direction}")
    if direction == "CW":
        param = "plus"
    else:
        param = "minus"
    subprocess.run(f"volumio volume {param}", shell=True)


if __name__ == "__main__":
    PIN_A = 23
    PIN_B = 24

    PIN_PREV = 13
    PIN_PLAY = 6
    PIN_NEXT = 5

    re0 = RotaryEncoder(PIN_A, PIN_B, (on_volume_change, None))
    re0.start()

    swb = SwitchBorad(
        PIN_PREV,
        PIN_PLAY,
        PIN_NEXT,
        on_prev_pushed=(on_prev_pushed, None),
        on_play_pushed=(on_play_pushed, None),
        on_next_pushed=(on_next_pushed, None),
    )
    swb.start()

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass
    finally:
        re0.stop()
        swb.stop()
        re0.join()
        swb.join()


4. systemdサービス化

ラズパイ起動時に自動的にスクリプトが起動するように、サービス化する。

まず、my-volumio-controller.serviceというファイルを作成。

[Unit]
Description=My Volumio Controller Service
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/volumio/volume.py
Restart=on-failure
User=volumio
WorkingDirectory=/home/volumio

[Install]
WantedBy=multi-user.target

/etc/systemd/system/にコピー。

sudo cp ./my-volumio-controller.service /etc/systemd/system/
sudo chmod 755 /etc/systemd/system/my-volumio-controller.service
sudo chown root:root /etc/systemd/system/my-volumio-controller.service

サービス有効化。

sudo systemctl enable my-volumio-controller.service

ラズパイを再起動して、有効化されているか操作して動作確認→OK。


5. 完成

機能はシンプルだが、操作が楽になり、ぐちゃぐちゃだったケーブルも整理され、ホコリも入りづらくなったので、完成とする。

https://cdn-ak.f.st-hatena.com/images/fotolife/d/dekuo-03/20250614/20250614230914.jpg



まとめと今後の課題

ラズパイオーディオ筐体の操作プログラムを作成し、完成させた。操作が快適になり、オーディオライフが充実しそうだ。