試してみたブログ

AI関連・iPhone/Pixelなどのガジェット・音声入力・サーマルプリンタなど興味をある事をどんどん試してみた際の記録

ゲームチケットの作成と終了通知を運用する

tameshitemita.blog

tameshitemita.blog

背景

  • ゲーム利用チケットの構想と、Home Assisntantを入れてAlexaの通知を実験を経て運用に向けて準備する

やることまとめ

  • 「iPadでアイコンタップ」はおなじみ、ショートカットアプリでsshでコマンドを叩きにいくのを想定
  • 「終了予定時間を入力」は、こどもに手間がかかる為、今回は45分という固定にした
  • チケットのフォーマットを決める+チケットのサーマルプリンタ出力を作る

試してみた

  • 逆順で対応していく
  • プリンタ出力+文言を決めて、Pythonで作成
from escpos.printer import Serial
from PIL import Image, ImageDraw, ImageFont
from datetime import datetime
import sys
import argparse
import locale

# 日本語曜日を使いたい場合(環境に合わせて必要なら有効化)
try:
    locale.setlocale(locale.LC_TIME, "ja_JP.UTF-8")
except locale.Error:
    # ロケールが無い環境では英語曜日になる
    pass


def wrap_line_pixel(line, font, max_width, draw):
    """
    1行のテキストを、pixel幅 max_width で折り返した行リストに変換する
    """
    result = []
    buf = ""
    for ch in line:
        trial = buf + ch
        bbox = draw.textbbox((0, 0), trial, font=font)
        w = bbox[2] - bbox[0]
        if w <= max_width:
            buf = trial
        else:
            if buf:
                result.append(buf)
            buf = ch
    if buf:
        result.append(buf)
    return result


def print_ticket(end_time_str: str):
    # フォント設定
    font_path = "/usr/share/fonts/truetype/fonts-japanese-gothic.ttf"
    font_title = ImageFont.truetype(font_path, 48)  # タイトル用(大きめ)
    font_item = ImageFont.truetype(font_path, 28)
    font_date = ImageFont.truetype(font_path, 24)

    # 用紙幅とレイアウト
    WIDTH = 576
    head_margin = 10
    line_margin = 6
    foot_margin = 20

    # 一時イメージ(幅計測用)
    tmp_img = Image.new("1", (WIDTH, 100), 1)
    tmp_draw = ImageDraw.Draw(tmp_img)

    left_margin = 7
    right_margin = 7
    max_text_width = WIDTH - (left_margin + right_margin)

    # ---- ここから文面の定義 ----
    now = datetime.now()

    # YYYY/MM/DD(曜日)
    # ロケールが有効なら日本語曜日、だめなら英語曜日になる
    date_text = now.strftime("%Y/%m/%d(%a)")

    start_time_text = now.strftime("開始時間:%H:%M:%S")
    end_time_text = f"終了時間:{end_time_str}"

    notice_lines = [
        "注意事項:",
        "・仲良くゲームをすること。",
        "・仲良く出来なかった場合は、すぐに終了とする。",
    ]

    # 行ごとに高さ計測
    def text_height(font):
        bbox = tmp_draw.textbbox((0, 0), "あ", font=font)
        return (bbox[3] - bbox[1]) + line_margin

    title_h = text_height(font_title)
    date_h = text_height(font_date)
    item_h = text_height(font_item)

    # チケット全体の高さを計算
    # タイトル1行 + 日付1行 + 開始/終了時間2行 + 注意事項4行
    HEIGHT = (
        head_margin
        + title_h
        + date_h
        + item_h * (2 + len(notice_lines))
        + foot_margin
    )

    # 本番イメージ
    img = Image.new("1", (WIDTH, HEIGHT), 1)
    draw = ImageDraw.Draw(img)

    y = head_margin

    # タイトル(中央寄せ・幅一杯イメージ)
    title_text = "ゲームチケット"
    title_bbox = draw.textbbox((0, 0), title_text, font=font_title)
    title_w = title_bbox[2] - title_bbox[0]
    title_x = (WIDTH - title_w) // 2
    draw.text((title_x, y), title_text, font=font_title, fill=0)
    y += title_h

    # 2行目: 日付
    date_bbox = draw.textbbox((0, 0), date_text, font=font_date)
    date_w = date_bbox[2] - date_bbox[0]
    date_x = (WIDTH - date_w) // 2  # 中央寄せ
    draw.text((date_x, y), date_text, font=font_date, fill=0)
    y += date_h

    # 3行目: 開始時間
    draw.text((left_margin, y), start_time_text, font=font_item, fill=0)
    y += item_h

    # 4行目: 終了時間(引数)
    draw.text((left_margin, y), end_time_text, font=font_item, fill=0)
    y += item_h

    # 注意事項(折り返しあり)
    for line in notice_lines:
        wrapped = wrap_line_pixel(line, font_item, max_text_width, draw)
        for wline in wrapped:
            draw.text((left_margin, y), wline, font=font_item, fill=0)
            y += item_h

    # デバッグ用に画像保存
    img.save("ticket_thermal.png")

    # 印刷
    p = Serial(devfile="/dev/ttyACM0", baudrate=9600, timeout=1, dsrdtr=True)
    p._raw(b"\x1b@")
    p.image("ticket_thermal.png")
    p.cut()


def main():
    parser = argparse.ArgumentParser(description="ゲームチケット印刷")
    parser.add_argument(
        "end_time",
        help="終了時刻(例: 21:30)",
    )
    args = parser.parse_args()
    print_ticket(args.end_time)


if __name__ == "__main__":
    main()
  • ショートカットアプリでiPadから呼び出す

  • やっていることは、Alexaの呼出時間のYAMLを書き換えるシェル、APIで任意のアクションを有効化させる+APIYAMLをリロードさせる、上記で作成したgame.pyに終了時間を渡してサーマルプリンタからゲームチケットの印刷

  • ホーム画面に追加

  • 実際に出てくる紙

  • この紙を持ってこどもはゲームをする+終了時間になったら下記アラームがなって終了する

youtu.be