ポケモン30周年だし、ポケモン目覚まし時計をラズパイzeroで作る#1 機材セッティング

スポンサーリンク
Uncategorized

はじめに

ポケモン30周年だし、ポケモン目覚まし時計を作ろうと思った。

なぜ目覚ましなのかというと、普段はスマホのアラームを使っているんだけど、アラームは曜日で設定できるんだけど、祝日も早い時間に起こしてくるので、困っていた。

目覚まし時計アプリも軽く見てみたけど、同様だった。

だから、自分でカスタマイズできる目覚まし時計を作ろうと思った。

完成したものが実際に動いている様子はXへ投稿したので、良ければ見ていただければと思います。

ハードウェア側

まずは機材を用意しないといけない。

使用する機材は以下。

  • ラズパイ
    • 今回はRaspberry Pi Zero Wを使用。
  • micro usb (32G byte)
    • 家にあったもの
  • 棚に設置するコンセント
    • ニトリで購入(棚に目覚まし時計を置くと、コードが棚から出てきて煩わしいので購入した)
  • スピーカー
  • ラズパイの周辺機器セット(必要ならば)
  • ラズパイとスピーカーを接続させるための変換器
  • type BとUSBの接続ケーブル
    • 家にあったもの

僕が購入したAmazonのリンク先も参考に載せておきます👇

●Raspberry Pi Zero W

●棚に設置するコンセント(こちらはニトリで購入)

●スピーカー

●ラズパイの周辺機器セット(必要であれば)

こちらは無くても実装はできましたが、あっても損ではないと思います。特に画面に接続して操作したい場合は重宝する。

●ラズパイとスピーカーを接続させるための変換器

セッティングする

osを入れる。

osの入れ方はググると結構出てくるので、そちらを参考にしてほしい。

osを入れるときは、Raspberry Pi Zeroの項目とRaspberry Pi Zero 2Wの項目があるので、間違えないように注意。

今回の場合だとRaspberry Pi Zero Wを使用するのでRaspberry Pi ZeroのOSを選択する。

osを入れる時にwifiの設定などをしておくと後で楽だと思う。

セッティング自体は結構単純で、スピーカを接続するだけ。

スピーカーの真ん中にテラリウムピカチュウを置くと以下のようになり、完成する。

プログラムを組む

普段ラズパイの中でプログラミングや作業をするときはvs codeでssh接続をする。

しかし、今回使用するRaspberry Pi Zero Wはvs codeからsshで接続できないようで、vs code上でプログラムのコーディングができない。

仕方が無いので、ローカルPCでコーディングをしたのち、scpでファイル群をUploadする流れで対応する。

その他、困ったところとしてはclaude cliも入れることができないことだ。

デバッグの時間の短縮はありがたいので、claude cliが入れられないのは困った。

これも、基本はローカルPCで相談する形になる。

python

pythonのコードは以下のようになる。

#!/usr/bin/env python3
"""CLI alarm clock for Raspberry Pi Zero (mpg123 + USB speaker).

Distinguishes weekday / weekend / holiday(JP) alarm times, and plays a
different sound per day of the week.
"""
import argparse
import json
import os
import signal
import subprocess
import sys
import time
from datetime import datetime

import jpholiday

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
PID_FILE = "/tmp/alarm_clock.pid"
DEFAULT_SOUND = os.path.join(BASE_DIR, "sounds", "alarm.mp3")
DAY_NAMES = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
CATEGORIES = ["weekday", "weekend", "holiday"]

DEFAULT_CONFIG = {
    "schedules": {
        "weekday": {"time": "07:00", "enabled": True},
        "weekend": {"time": "09:00", "enabled": True},
        "holiday": {"time": "09:00", "enabled": True},
    },
    "sounds": {day: DEFAULT_SOUND for day in DAY_NAMES},
}


def load_config():
    if not os.path.exists(CONFIG_FILE):
        return json.loads(json.dumps(DEFAULT_CONFIG))
    with open(CONFIG_FILE, "r") as f:
        config = json.load(f)
    # backfill any missing keys (safe for older config files)
    for cat in CATEGORIES:
        config["schedules"].setdefault(cat, DEFAULT_CONFIG["schedules"][cat])
    for day in DAY_NAMES:
        config["sounds"].setdefault(day, DEFAULT_SOUND)
    return config


def save_config(config):
    with open(CONFIG_FILE, "w") as f:
        json.dump(config, f, indent=2, ensure_ascii=False)


def category_for_date(d):
    if jpholiday.is_holiday(d):
        return "holiday"
    return "weekday" if d.weekday() < 5 else "weekend"


def cmd_set(args):
    if args.category not in CATEGORIES:
        print(f"category must be one of {CATEGORIES}")
        sys.exit(1)
    try:
        datetime.strptime(args.time, "%H:%M")
    except ValueError:
        print("time must be HH:MM")
        sys.exit(1)

    config = load_config()
    config["schedules"][args.category]["time"] = args.time
    save_config(config)
    print(f"{args.category} alarm set to {args.time}")


def cmd_toggle(args, enabled):
    if args.category not in CATEGORIES:
        print(f"category must be one of {CATEGORIES}")
        sys.exit(1)
    config = load_config()
    config["schedules"][args.category]["enabled"] = enabled
    save_config(config)
    print(f"{args.category} alarm {'enabled' if enabled else 'disabled'}")


def cmd_sound(args):
    if args.day not in DAY_NAMES:
        print(f"day must be one of {DAY_NAMES}")
        sys.exit(1)
    config = load_config()
    config["sounds"][args.day] = args.path
    save_config(config)
    print(f"{args.day} sound set to {args.path}")


def cmd_show(args):
    config = load_config()
    print("schedules:")
    for cat in CATEGORIES:
        s = config["schedules"][cat]
        status = "ON" if s["enabled"] else "off"
        print(f"  {cat:8s} {s['time']} [{status}]")
    print("sounds:")
    for day in DAY_NAMES:
        print(f"  {day}: {config['sounds'][day]}")

    today = datetime.now().date()
    print(f"today ({today}) category: {category_for_date(today)}")


def cmd_stop(args):
    if not os.path.exists(PID_FILE):
        print("no alarm currently sounding")
        return
    with open(PID_FILE) as f:
        pid = int(f.read().strip())
    try:
        os.killpg(os.getpgid(pid), signal.SIGTERM)
        print("alarm stopped")
    except ProcessLookupError:
        print("alarm process already gone")
    os.remove(PID_FILE)


def play_sound(sound_path):
    proc = subprocess.Popen(
        ["mpg123", "--loop", "-1", sound_path],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        preexec_fn=os.setsid,
    )
    with open(PID_FILE, "w") as f:
        f.write(str(proc.pid))
    return proc


def cmd_run(args):
    print("alarm loop started, checking every 20s. Ctrl+C to quit.")
    fired_this_minute = set()
    while True:
        now = datetime.now()
        current_time = now.strftime("%H:%M")
        minute_key = now.strftime("%Y%m%d%H%M")
        category = category_for_date(now.date())
        day_name = DAY_NAMES[now.weekday()]

        config = load_config()
        schedule = config["schedules"][category]
        if schedule["enabled"] and schedule["time"] == current_time:
            key = (category, minute_key)
            if key not in fired_this_minute:
                fired_this_minute.add(key)
                sound = config["sounds"][day_name]
                print(f"[{now}] firing {category} alarm, playing {sound}")
                play_sound(sound)

        fired_this_minute = {k for k in fired_this_minute if k[1] == minute_key}
        time.sleep(20)


def main():
    parser = argparse.ArgumentParser(description="Raspberry Pi alarm clock")
    sub = parser.add_subparsers(dest="command", required=True)

    p_set = sub.add_parser("set", help="set alarm time for a category")
    p_set.add_argument("category", choices=CATEGORIES)
    p_set.add_argument("time", help="HH:MM 24h format")
    p_set.set_defaults(func=cmd_set)

    p_enable = sub.add_parser("enable", help="enable a category's alarm")
    p_enable.add_argument("category", choices=CATEGORIES)
    p_enable.set_defaults(func=lambda args: cmd_toggle(args, True))

    p_disable = sub.add_parser("disable", help="disable a category's alarm")
    p_disable.add_argument("category", choices=CATEGORIES)
    p_disable.set_defaults(func=lambda args: cmd_toggle(args, False))

    p_sound = sub.add_parser("sound", help="set the sound file for a day of week")
    p_sound.add_argument("day", choices=DAY_NAMES)
    p_sound.add_argument("path", help="path to mp3")
    p_sound.set_defaults(func=cmd_sound)

    p_show = sub.add_parser("show", help="show current config")
    p_show.set_defaults(func=cmd_show)

    p_stop = sub.add_parser("stop", help="stop a currently sounding alarm")
    p_stop.set_defaults(func=cmd_stop)

    p_run = sub.add_parser("run", help="run the alarm checking loop (foreground)")
    p_run.set_defaults(func=cmd_run)

    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()

大まかな機能としては、以下である。

  • 平日・土日・祝日でアラーム時刻を変えられる
  • 曜日ごとに再生する音楽を変えられる

主に使われる関数を幾つか紹介する。


load_config() 関数

設定ファイル(config.json)を読み込む関数。


save_config() 関数

configファイルを保存する関数。

例えば

$ alarm_clock.py set weekday 06:30

と実行すると、

{
    "weekday":{
        "time":"06:30"
    }
}

が保存される。


category_for_date() 関数

ある日付が平日、土日、祝日のどれか判定する。

例えば2026/1/1なら「holiday」になる。


cmd_sound() 関数

例えば、

$ alarm_clock.py sound mon music.mp3

を実行すると、月曜日だけmusic.mp3にできる。


play_sound() 関数

ここで実際に音を鳴らしている。

mpg123 --loop -1 alarm.mp3をsubprocessを使って実行している。

最後に

次回は実際に動かしてみる。

タイトルとURLをコピーしました