10
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?

IMG_0044.jpg

こんにちは、普段ローコード系開発プロジェクトでスクラムマスタをしているDaigorianです。
この記事の内容は会社の仕事とは全く関係ない趣味の電子工作ネタですが、開発するに至った動機がちょっとだけ育児休業に関連するので、育児休業の感想も添えて会社のアドベントカレンダーに乗せちゃうことにしました。

年末年始は電子工作楽しみましょう!

開発動機:ミルクは2時間しかもたない

新生児は昼夜問わず2〜3時間ごとにミルクを飲みます。またミルクは一度口をつけると“2時間以内”に飲み切る必要があり、時間が過ぎたミルクは破棄しなければなりません。

特に夜のミルク当番は朦朧とした意識の中で対応するため、赤ちゃんが泣いたタイミングで飲みかけのミルクが手元にあると「あれ?これ飲ませていいやつだっけ?」となりがちです。

そこで期限切れのミルクがすぐにわかるように、ボタン1つで調乳時刻を記録できる仕組みを開発しました。

FrTIw17acAEmDZK.jpg

“1Push日付ラベルプリンタ”の誕生

それがこちらの装置。ボタンを押すと、その瞬間の時刻をラベルプリンタで即座に印刷します。
IMG_0043.gif

ちょっとふざけた機械に見えるかもしれませんがこれ本当に便利でした。
「調乳する → ボタンを押す → 哺乳瓶に貼る」 というルーチンは卒乳まで続き、
その後も食品の開封日の記録など思いがけない場面で役立ち続けています。

今日はそんな装置の作り方を紹介したいと思います。

作り方

材料

  • Phomemo スマートラベルプリンタ M110 @ ¥5,556
  • Raspberry Pi Zero W @ ¥2,737
  • Raspberry Pi Zero ケース @ ¥500
  • Micro USB (オス) - Micro USB (オス) OTGケーブル @ ¥849
  • LED付き押しボタン (SparkFun COM-10439 LED Tactile Button- White)  @ ¥400
  • 1/4W100Ω 抵抗 @ ¥150(100本入り)
  • ピンヘッダー (L型) 1×2 @ ¥10 x 2個
  • 2ピンコネクタ付きケーブル メス-メス @ ¥80
  • SDカード ありもの
  • 半田すこしと半田ごて

...2年前はもっと安かったんですがだいぶ高くなりましたね。
特にプリンタは2,000円台だったと記憶しているので別の機種を選んでもいいかもしれません。

ハードウェアの準備

まずはボタンを準備します。LED付きボタンを使うことで、1入力1出力できるようになり、ユーザ体験がリッチになります。またハードウェアPWMが使えるPINをLEDに割り当てることで点滅や輝度調整ができさらに表現がひろがります。

GPIO 13 (PWM) を LED Output、 GPIO 27 をButton Inputにしています。。
image.png

L型ピンヘッダを使い、ケースへの干渉を避けつLEDボタンの脱着を実現します。
IMG_0037.jpg

公式ケースのカメラモジュール用穴とLEDの直径が奇跡的にぴったりなので、カメラモジュール基盤と同サイズの取付アダプタを作成してLEDとケースを結合します。
image.png
IMG_0040.jpg

LEDボタンを接続してケースを閉めれば完成
IMG_0041.jpg

ソフトウェアの準備

OSをセットアップ

Raspberry Pi Imager でSDカードにOSを書き込みます

  • デスクトップ環境無しのLiteイメージを使う
  • Wifi セットアップあり
  • SSH ログインあり

image.png

LEDボタンの動作確認

最近のラズパイでは GPIO操作に libgpiod を使うべきですが、pigpioのほうがまだまだ便利でPWM波形もきれいに出るのでpigpioを使います。

まずはpigpioのインストールとサービス起動

pi@label-printer:~ $ sudo apt update
pi@label-printer:~ $ sudo apt install pigpio python3-pigpio
pi@label-printer:~ $ sudo systemctl enable pigpiod
pi@label-printer:~ $ sudo systemctl start pigpiod

ボタンとLEDの動作確認します。

button_led_test.py
import pigpio
pi = pigpio.pi()

# ボタン接続PINを入力にしてプルアップ
pi.set_mode(27, pigpio.INPUT)
pi.set_pull_up_down(27, pigpio.PUD_UP)

# LEDはOUTPUT
pi.set_mode(13, pigpio.OUTPUT)


# ボタンを押すとLEDが光る
pi.wait_for_edge(27, pigpio.FALLING_EDGE, 30)
pi.write(13, 1)

# ボタンを離すとLEDが消える
pi.wait_for_edge(27, pigpio.RISING_EDGE, 30)
pi.write(13, 0)

このテストコードを実行して、ボタンを押している間だけLEDが光れば正常です。

プリンタドライバのビルドとインストール

Phomemoのサーマルプリンタのドライバはこちらのプロジェクトで開発されています。
https://github.com/vivier/phomemo-tools
実はPhomemoM110の実装は私もContribute(1,2)してます。
普段ローコードでGit使わないのでおそらく酷いPRだったはずですが、プロジェクトオーナーのVivier氏が優しくてよかった。あとUSBパケットダンプおもしろかった。

pi@label-printer:~ $ # 依存パッケージのインストール
pi@label-printer:~ $ sudo apt-get install cups python3-pip python3-pil  # それなりに時間かかります
pi@label-printer:~ $ sudo pip install pyusb --break-system-packages   # CUPSドライバがpyusbを使うので break-system-packagesやむなし
pi@label-printer:~ $ # プリンタドライバ本体のダウンロードとコンパイル インストール
pi@label-printer:~ $ wget https://github.com/vivier/phomemo-tools/archive/refs/heads/master.zip
pi@label-printer:~ $ unzip master.zip
pi@label-printer:~ $ cd phomemo-tools-master/
pi@label-printer:~ $ cd phomemo-tools-master/
pi@label-printer:~/phomemo-tools-master $ cd cups/
pi@label-printer:~/phomemo-tools-master/cups $ make
pi@label-printer:~/phomemo-tools-master/cups $ sudo make install
pi@label-printer:~/phomemo-tools-master/cups $ cd
pi@label-printer:~ $ # USB接続されたプリンタを M110として登録する
pi@label-printer:~ $ sudo lpadmin -p M110 -E -v serial:/dev/usb/lp0  -P /usr/share/cups/model/Phomemo/Phomemo-M110.ppd.gz
pi@label-printer:~ $ # いったんリブート
pi@label-printer:~ $ sudo reboot
pi@label-printer:~ $ # テストプリント
pi@label-printer:~ $ echo "Test"  | lp -d M110 -o media=w30h20

Test と書かれたラベルが印刷できれば成功です。

ボタン待ち受け&プリントデーモンの配置

ボタンを押したら印刷を開始するPythonスクリプトを書いていきます。

/usr/local/bin/print_daemon.py
#!/usr/bin/env python3
import pigpio
import subprocess
import time
import signal
import sys

# --------------------------------------------------------
# 終了シグナル管理
# --------------------------------------------------------
terminate = False

def handle_sigterm(signum, frame):
    """
    SIGTERM / SIGINT 受信時に terminate フラグを立てて
    メインループを安全に抜けるためのハンドラ。
    """
    global terminate
    print("Received signal:", signum)
    terminate = True

# systemdサービス等で送られるSIGTERMに対応
signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGINT, handle_sigterm)


# --------------------------------------------------------
# GPIO設定
# --------------------------------------------------------
led_pin = 13   # LED ピン番号 (PWM 出力)
btn_pin = 27   # ボタン入力ピン

pi = pigpio.pi()
if not pi.connected:
    raise RuntimeError("pigpiod が動いていません")

# --- ボタンのチャタリング対策(30msフィルタ) ---
pi.set_glitch_filter(btn_pin, 30000)

# --- ボタン設定 ---
pi.set_mode(btn_pin, pigpio.INPUT)
pi.set_pull_up_down(btn_pin, pigpio.PUD_UP)
SHORT_PRESS_SEC = 3  # 長押し判定の境界秒数


# --------------------------------------------------------
# LED クラス
# PWM による明るさ制御・点滅・フェードアウトを管理
# --------------------------------------------------------
class LED:
    def __init__(self, pi, pin):
        """
        LED 初期化。
        PWM 周波数・レンジを設定し、輝度を保持。
        """
        self.pi = pi
        self.pin = pin
        self.brightness = 255

        pi.set_mode(pin, pigpio.OUTPUT)
        pi.set_PWM_frequency(pin, 800)
        pi.set_PWM_range(pin, 255)

    def on(self):
        """LED を最大輝度で点灯。"""
        self.brightness = 255
        self.pi.set_PWM_dutycycle(self.pin, self.brightness)

    def standby(self):
        """スタンバイ状態の弱点灯(低輝度)。"""
        self.brightness = 10
        self.pi.set_PWM_frequency(self.pin, 800)
        self.pi.set_PWM_dutycycle(self.pin, self.brightness)

    def off(self):
        """LED 消灯。"""
        self.brightness = 0
        self.pi.set_PWM_dutycycle(self.pin, self.brightness)

    def brink(self):
        """
        LED 点滅(作業中アニメーション)。
        周波数 1Hz、50% デューティで点滅。
        """
        self.pi.set_PWM_frequency(self.pin, 1)
        self.pi.set_PWM_dutycycle(self.pin, 128)
        self.brightness = 255  # fade_out のため基準値に戻す

    def fade_out(self, duration=1.5, steps=300):
        """
        LED の明るさを徐々に減衰させて自然に消灯する演出。

        Parameters:
            duration (float): フェードアウト全体の秒数
            steps (int): 明るさ変化ステップ数
        """
        self.pi.set_PWM_frequency(self.pin, 800)
        for i in range(steps):
            duty = self.brightness - int(self.brightness * i / steps)
            self.pi.set_PWM_dutycycle(self.pin, duty)
            time.sleep(duration / steps)
        self.off()


# LED 操作用インスタンス
led = LED(pi, led_pin)


# --------------------------------------------------------
# 印刷処理
# --------------------------------------------------------
def print_timestamp():
    """
    シールプリンタに現在時刻を印刷。
    lp コマンド経由でプリンタ M110 に送信。
    """
    cmd = (
        'echo "\\nTimestamp\\n`date +\\"%4Y/%2m/%2d\\n  %H:%M\\"`" '
        '| lp -d M110 -o media=w30h20'
    )
    result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    print("return:", result.returncode)        # 終了コード
    print(result.stdout.decode())              # 標準出力
    print(result.stderr.decode())              # 標準エラー


# --------------------------------------------------------
# メインループ
# --------------------------------------------------------
try:
    while not terminate:

        # 初期状態は弱点灯
        led.standby()

        # ボタン押下(FALLING)されるまでブロック。1秒でタイムアウト。
        if pi.wait_for_edge(btn_pin, pigpio.FALLING_EDGE, 1):
            print("FALLING (pressed)")
            led.on()  # 押している間は強点灯

            # 短押し判定
            if pi.wait_for_edge(btn_pin, pigpio.RISING_EDGE, SHORT_PRESS_SEC):
                print("SHORT PRESS")
                led.brink()  # 再び初期状態に戻るまで点滅を続ける
                print_timestamp()

            else:
                # 規定秒押され続けた → 長押し
                print("LONG PRESS CONFIRMED")
                led.brink()   # 長押し成立を知らせる点滅
                print("Waiting for release to shutdown...")

                # 長押し成立後3秒以内にボタンを離せばシャットダウン
                if pi.wait_for_edge(btn_pin, pigpio.RISING_EDGE, SHORT_PRESS_SEC):
                    print("Shutdown requested")
                    subprocess.run("shutdown -h now", shell=True)
                else:
                    # 長押し成立後、さらに3秒以上押し続ければシャットダウンキャンセル
                    print("Shutdown canceled")

finally:
    # プログラム終了時はLEDをフェードアウトしてから pigpio を解放
    led.fade_out()
    pi.stop()

単体で動作することを確認します。
実行権限をつけるの忘れずに。

pi@label-printer:~ $ sudo chmod +x /usr/local/bin/print_daemon.py
pi@label-printer:~ $ /usr/local/bin/print_daemon.py
FALLING (pressed)                                 ←ボタン短押し
SHORT PRESS
return: 0
request id is M110-7 (0 file(s))                  ←ラベルが印刷される
FALLING (pressed)                                 ←ボタン長押し
LONG PRESS CONFIRMED
Waiting for release to shutdown...                ←ボタン長押し維持
Shutdown canceled
^CReceived signal: 2                              ←Ctrl + C で停止
pi@label-printer:~ $

単体動作が確認取れたらさらにこのPythonスクリプトをサービスとして起動するために Systemd の Service ユニットファイル を置きます。

/etc/systemd/system/print-daemon.service
[Unit]
Description=Label Printer Button Daemon
After=network.target multi-user.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/env python3 /usr/local/bin/print_daemon.py
WorkingDirectory=/usr/local/bin
StandardOutput=journal
StandardError=journal
Restart=no
User=root
Group=root

# GPIO や CUPS/USB が準備されていることを保証する
ExecStartPre=/bin/sleep 2

[Install]
WantedBy=multi-user.target

サービス起動し、自動起動するように設定します。

pi@label-printer:~ $ sudo systemctl enable print-daemon
pi@label-printer:~ $ sudo systemctl start print-daemon
pi@label-printer:~ $ sudo systemctl status print-daemon

ここまで終わるとLEDが薄暗く光ってスタンバイ状態になっているかと思います。
この状態で以下の動作が問題なく動けば成功です。

  • ボタン短押しで時刻が印刷されること
  • ボタン長押し(3秒以上6秒未満)でラズパイがシャットダウンされること
  • ボタンさらに長押し(6秒以上)でシャットダウンがキャンセルされること

オーバレイファイルシステムに切り替え

ここまで終わったら不意なシャットダウンやSDカードの消耗などでシステムが壊れないように、オーバレイファイルシステムに切り替えライトプロテクトをかけます。

pi@label-printer:~ $ sudo raspi-config
# 4 Performance Options  Configure performance settings
# P2 Overlay File System Enable/disable read-only file system
# Would you like the overlay file system to be enabled?  >Yes
# Would you like the boot partition to be write-protected? > Yes

再起動すれば完成です!

最後に育休の感想など

上記の工作は2年前にうちの子供が生まれる前に作成したものを今のラズパイ環境にアレンジしたものです。

私も当時育休を3か月いただいたのですが、当時を思い返してみると余裕があったのはあかちゃんお迎え前するまでの数日間だけ、おうちにお迎えしてからは怒涛の忙しさであっという間に育休が過ぎていった印象でした。

でもこのいわゆる「育児スタートダッシュ期間」がなければ、パパがママと同じ程度に一通りの育児対応ができる状態、いわゆる「ワンオペ」に耐えられる体制を築くのは難しかったので子育てチームとしてはとても重要なで必要不可欠な期間でした。

あとこの時期の子供をこれだけ相手できる時間は本当に貴重です

うちの会社は育休取得率が100%だそうなので、今後も素晴らしい記録が続いてほしいです。

10
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
10
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?