秋月時計(大型) その2時計アプリ編

RaspberryPi

時計アプリ構築

プログラムの下調べはできたので、時計表示アプリを作成していく。 7セグの表示については、OnOffを配列化しておく。 各種値は変数化しておき、プログラムもなるべく小さくなるように最適化をして、簡易版が完成。

#!/usr/bin/env python3
 
import RPi.GPIO as GPIO # RPi.GPIOモジュールを使用
import sys
import time
import datetime
from zoneinfo import ZoneInfo
 
# GPIO番号定義
gpio_keta = [8, 7, 25, 5]
gpio_7seg = [6, 13, 26, 21, 20, 19, 16]
gpio_koron = 22
gpio_dot = 18
gpio_yellow = 24
gpio_X = 23
gpio_Y = 12
 
# WaitTime
wait_time = 0.004
 
# 7seg表示用
gpio_pin = [
  [1, 1, 1, 1, 1, 1, 0],
  [0, 1, 1, 0, 0, 0, 0],
  [1, 1, 0, 1, 1, 0, 1],
  [1, 1, 1, 1, 0, 0, 1],
  [0, 1, 1, 0, 0, 1, 1],
  [1, 0, 1, 1, 0, 1, 1],
  [1, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 0, 0, 0, 0],
  [1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 1, 1]
]
 
# 初期処理
def setup():
  # GPIO番号指定の準備
  GPIO.setmode(GPIO.BCM)
 
  # GPIO出力に設定
  for i in range(0, 4):
    GPIO.setup(gpio_keta[i], GPIO.OUT)
  for i in range(0, 7):
    GPIO.setup(gpio_7seg[i], GPIO.OUT)
  GPIO.setup(gpio_koron    , GPIO.OUT)
  GPIO.setup(gpio_dot      , GPIO.OUT)
  GPIO.setup(gpio_yellow   , GPIO.OUT)
  GPIO.setup(gpio_X        , GPIO.OUT)
  GPIO.setup(gpio_Y        , GPIO.OUT)
  # Clear
  for i in range(0, 4):
    GPIO.output(gpio_keta[i], GPIO.LOW)
  for i in range(0, 7):
    GPIO.output(gpio_7seg[i], GPIO.LOW)
 
# 表示桁を選択
def selectDigit(digit):
  for i in range(0, 4):
    GPIO.output(gpio_keta[i],GPIO.HIGH if (digit == 0x01<<i) else GPIO.LOW)
 
# 7seg数値表示
def setNumber(val):
  for i in range(0, 7):
    GPIO.output(gpio_7seg[i], GPIO.LOW if (gpio_pin[val][i] == 0) else GPIO.HIGH)
 
# 終了処理
def destroy():
  # GPIOを解放
  GPIO.cleanup()
  sys.exit()
 
# 表示ループ
def loop():
  while True:
    # 現在時刻取得
    dt = datetime.datetime.now(ZoneInfo("Asia/Tokyo"))
    hh = str(dt.hour).rjust(2, '0')
    mm = str(dt.minute).rjust(2, '0')
    ms = dt.microsecond
    # 1桁目
    setNumber(int(mm[1]))
    selectDigit(1)
    time.sleep(wait_time)
    # 10桁目
    setNumber(int(mm[0]))
    selectDigit(2)
    time.sleep(wait_time)
    # 100桁目
    setNumber(int(hh[1]))
    selectDigit(4)
    time.sleep(wait_time)
    # 1000桁目
    setNumber(int(hh[0]))
    selectDigit(8)
    time.sleep(wait_time)
    # コロン表示
    GPIO.output(gpio_koron, GPIO.LOW if (ms >= 500000) else GPIO.HIGH)
   
if __name__ == '__main__':
  # start clock print
  setup()
  try:
    loop()
    destroy()
  except KeyboardInterrupt:
    # Ctrl + C
    print("Keybord Interrupt occurred!")
    destroy()
  finally:
    print("Finished!")
    destroy()

現在時刻取得時にマイクロ秒までわかる。マイクロなので0~999,999まで6桁の値のはず。 なので、0~500,000までなら真ん中のコロン表示のLEDを点灯し、それ以外なら消灯させてみると、 いい感じに1秒周期での点滅をしてくれた。時計が動いている感じがしてよい。

これで、ひとまず時計として現在時刻を表示する機能は完成できた。

ケースへの格納

表示板や制御基板やラズベリーパイをひとまず入れていた黒い箱だが、 便利なのでこの箱に時計表示器を取り付けて、箱ごと一体型の時計にしてしまおう。 ラズベリーパイや秋月時計が電線で無様につながっているので、それぞれ固定化して、 取り付けてしまおうと思う。

まずは、表示部の取り付け。表示基盤の四隅にはあらかじめ取付用のねじ穴らしきものが開いているので、これを活用。 使用するのは、ペテット。これはねじ穴が上面にあり、下面は両面テープとなっているので、ねじ穴を取り付けることができる。

このように、表示部の基盤とペテットをねじで止めてやればよい。このままペテットの両面テープを使って、 好きなところに取り付けられる。またねじ止めなので、取り外すことも可能。便利。

黒箱内の左上位置に表示部を取り付けてみた。

制御基板やラズベリーパイも同様にペテット取り付けで、箱内に配置。 ケーブルが少し邪魔だなぁ。

ラズベリーパイは右側に配置するが、ラズパイ1b+なので電源線とLANケーブルの接続は必須。 これらの取り回しを考慮して、どのように配置しようか悩む。
いっそのこと、箱の横からケーブルをつないでしまおうと、箱に穴をあけることにした。

ラズベリーパイを横向きに配置することとし、だいたいの良い位置にLANケーブルを通す穴をあけるため、 マステで大体の位置を設定。このへんい穴をあけてやればよさそうだ。

と思ったのだが、箱の外側を見ると穴をあけづらい場所だった。困った、もう少し真ん中寄りじゃないと、難しいな。

ということで、ラズベリーパイをもう少しかさ上げすべく、下駄を履かせてやることに。 15mmのジョイントスペーサーがあったので、ペテットとの取り付けには、こいつを間にかませてやり、底上げする。

少し足が伸びた状態で、位置決めをして、マステで穴あけ位置を特定しておく。

穴あけ位置が決まったら、ドリルで穴開けを実施。

穴あけのセンターが決まったら、今度は外側から20mmのホールソーで大きな穴開けを行う。 ステップドリルで穴を広げていってもよかったのだが、ちょうどよさそうなホールソーがあったのでこれを使ってみた。 プラスチックの箱なので、割れないように、また熱で溶けないように慎重に穴あけを実施。

無事、きれいに穴が開いて、標準タイプのLANコネクタを通すことができた。 これで大丈夫だろう。

ラズベリーパイも貫通穴位置に合わせて最終的な位置合わせをして、ペテットを貼り付ける。

残像対応

よく見てみると、7セグ表示に少し残像が出ている。 ひとつ前の桁の表示が、次の桁に薄く表示しているようだ。
下のように、先頭桁は「1」表示なのだが、4桁目の「0」がうっすら出ている。

表示処理に問題があるだろうと、プログラムを見ると
①7セグの項目セット
②桁表示指定
③wait
④(①にもどる)7セグの項目セット
となっているが、前の桁が表示中の状態で①が動作し一瞬だけ次の桁の表示をしてしまい、 ②で次の桁を表示ということが起きているようで、①~②間で間違った表示が出ていると思われる。

なので、①の7セグ内容設定の前に一度表示を切って、それから7セグ設定を行い、 指定の桁を表示させるといった動作に変更すると、残像表示はなくなった。 一瞬の表示だと思うのだが、案外薄く見えてしまうので、しっかり必要な時だけ点灯させるように処理しなければいけなかった。

~抜粋
    # 1桁目
    selectDigit(0)
    setNumber(int(mm[1]))
    selectDigit(1)
    time.sleep(wait_time)
~

これで、おかしな残像表示が出ることなく、時計表示ができるようになった。

常駐化

うまく動作するようなので、ラズパイ起動と同時に常駐化させたい。 常駐化させるにはsystemdへの登録となるので、まずはサービスファイルを作成
sudo nano /etc/systemd/system/piclock.service

[Unit]
Description=RaspberryPi Clock Service App
  
[Service]
ExecStart=/usr/bin/python3 /home/pi/share/clock/clock.py
Restart=no
Type=simple
  
[Install]
WantedBy=multi-user.target

これで、事前準備は完了。

ファイルの権限類はいろいろ上げておこう。
sudo chown root:root /etc/systemd/system/piclock.service
sudo chmod 755 /etc/systemd/system/piclock.service

ひとまず、systemctlを再ロード
sudo systemctl daemon-reload

では、認識されているか確認してみよう。

$ systemctl list-unit-files --type=service | grep piclock

piclock.service                            disabled        enabled

認識されているようだ。では、起動時に自動動作するように設定

$ sudo systemctl enable piclock

Created symlink /etc/systemd/system/multi-user.target.wants/piclock.service → /etc/systemd/system/piclock.service.

もう一度、設定確認

$ systemctl list-unit-files --type=service | grep piclock

piclock.service                            enabled         enabled

最初はdisabledだった部分がenabledに変化しており、自動起動設定もできている。 これで、ラズパイ起動時に時計表示プログラムが動作する。

ここで少し問題が発覚。systemdでのstopでは、終了処理が動作してないことが判明。
どうも、systemd管理下で動作させているが、systemctl stop で処理終了させたときは、 終了コマンドが処理されておらず、強制的に停止している様子。なのでGPIOが解放されていない? あと7セグ表示も最後に表示中だったと思われる1桁が表示したまま残る。

こちらの記事 をみると、ちょうどこの問題がかいてあった。 systemdでの終了時はSIGTERMというメッセージ処理にて終了するようで、try~finallyの記述をしても引っかからないらしい。 なので、シグナルを受け取る処理を入れておいて、終了シグナル時の終了処理を記述する必要があるようだ。
signal.signal(signal.SIGTERM, termed)
で終了処理のシグナルが来たらtermd関数を呼び出すように設定しておき、termd関数内で正しく終了処理を呼び出すようにした。

ということで、常駐向けの時計処理としては最終的にはこうなった

#!/usr/bin/env python3
 
import RPi.GPIO as GPIO # RPi.GPIOモジュールを使用
import sys
import signal
import time
import datetime
from zoneinfo import ZoneInfo
 
# GPIO番号定義
gpio_keta = [8, 7, 25, 5]
gpio_7seg = [6, 13, 26, 21, 20, 19, 16]
gpio_koron = 22
gpio_dot = 18
gpio_yellow = 24
gpio_X = 23
gpio_Y = 12
 
# WaitTime
wait_time = 0.004
 
# 7seg表示用
gpio_pin = [
  [1, 1, 1, 1, 1, 1, 0],
  [0, 1, 1, 0, 0, 0, 0],
  [1, 1, 0, 1, 1, 0, 1],
  [1, 1, 1, 1, 0, 0, 1],
  [0, 1, 1, 0, 0, 1, 1],
  [1, 0, 1, 1, 0, 1, 1],
  [1, 0, 1, 1, 1, 1, 1],
  [1, 1, 1, 0, 0, 0, 0],
  [1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 0, 1, 1],
  [0, 0, 0, 0, 0, 0, 0]
]
 
# 初期処理
def setup():
  # GPIO番号指定の準備
  GPIO.setmode(GPIO.BCM)
 
  # GPIO出力に設定
  for i in range(0, 4):
    GPIO.setup(gpio_keta[i], GPIO.OUT)
  for i in range(0, 7):
    GPIO.setup(gpio_7seg[i], GPIO.OUT)
  GPIO.setup(gpio_koron    , GPIO.OUT)
  GPIO.setup(gpio_dot      , GPIO.OUT)
  GPIO.setup(gpio_yellow   , GPIO.OUT)
  GPIO.setup(gpio_X        , GPIO.OUT)
  GPIO.setup(gpio_Y        , GPIO.OUT)
  # Clear
  for i in range(0, 4):
    GPIO.output(gpio_keta[i], GPIO.LOW)
  for i in range(0, 7):
    GPIO.output(gpio_7seg[i], GPIO.LOW)
 
# 表示桁を選択
def selectDigit(digit):
  for i in range(0, 4):
    GPIO.output(gpio_keta[i],GPIO.HIGH if (digit == 0x01<<i) else GPIO.LOW)
 
# 7seg数値表示
def setNumber(val):
  for i in range(0, 7):
    GPIO.output(gpio_7seg[i], GPIO.LOW if (gpio_pin[val][i] == 0) else GPIO.HIGH)
 
# 終了処理
def destroy():
  # 表示停止
  selectDigit(0)
  setNumber(10)
  # GPIOを解放
  GPIO.cleanup()
  sys.exit()
 
# シグナル終了
def termed(signum, frame):
    print("SIGTERM!")
    destroy()
 
# 表示ループ
def loop():
  signal.signal(signal.SIGTERM, termed)
  while True:
    dt = datetime.datetime.now(ZoneInfo("Asia/Tokyo"))
    hh = str(dt.hour).rjust(2, '0')
    mm = str(dt.minute).rjust(2, '0')
    ms = dt.microsecond
    # 1桁目
    selectDigit(0)
    setNumber(int(mm[1]))
    selectDigit(1)
    time.sleep(wait_time)
    # 10桁目
    selectDigit(0)
    setNumber(int(mm[0]))
    selectDigit(2)
    time.sleep(wait_time)
    # 100桁目
    selectDigit(0)
    setNumber(int(hh[1]))
    selectDigit(4)
    time.sleep(wait_time)
    # 1000桁目
    selectDigit(0)
    setNumber(int(hh[0]))
    selectDigit(8)
    time.sleep(wait_time)
    # コロン表示
    GPIO.output(gpio_koron, GPIO.LOW if (ms >= 500000) else GPIO.HIGH)
   
if __name__ == '__main__':
  # start clock print
  setup()
  try:
    loop()
    destroy()
  except KeyboardInterrupt:
    # Ctrl + C
    print("Keybord Interrupt occurred!")
    destroy()
  finally:
    print("Finished!")
    destroy()