配線追加
秋月時計との接続ができて、7segやLED表示ができるようになったが、 よく見ると秋月時計の基板上にはボタンがある(4つ) 配線図を見ると、これらもpicにはプルアップ状態でつながっているので、 INPUT情報として、これらもラズベリーパイに引き込んでみよう。

スイッチがつながっているPICのピンは、固まって左側に存在しているので、引き出しやすい。 ラズベリーパイ側は、コロン表示をつないでいるIO22付近が空いているので、このあたりに接続しよう。

すでにいろいろつながっており、箱にも収納した状態で出しづらいので、箱内に装着した状態のまま、 追加のラインをはんだ付けしていく。ちょうど手前側のピンばかりなので、何とかなった。 導通確認すると、問題なくつながっているので、狭くて作業しづらい場所でのはんだ付けだが、うまくいってるようだ。
ということで、出力線だけだった秋月時計との接続に、入力線が加わった。 浮いている4つの白い線(見えづらい)が追加した配線だ。

入力検知
いままでの時計表示プログラムに、スイッチによる入力処理を追加する。 これまでは出力のみだったので、「GPIO.output」コマンドによる出力制御だったが、 あらたにスイッチによる入力制御をおこなう。入力の処理は、「GPIO.input」になる。 GPIO.setupで入力設定にしておいて、GPIO.inputで状態を参照する動きだ。
といっても、常時入力状態を監視しないといけないので、 ループ処理内でチェックを繰り返すことになるかと思っていたが、 入力状態の変化でイベントを起こしてくれる関数があった。
GPIO.add_event_detect
関数を使用して、入力状態の変化時にコールバック関数を呼ぶことができる。
例えば、GPIO17を入力として、入力変化で関数「push_event」を呼び出す場合は以下。
GPIO.setmode(GPIO.BCM) GPIO.setup(17 , GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(17, GPIO.FALLING, callback=push_event, bouncetime=200)
入力はプルアップ設定なので、通常はHigh。スイッチが押されるとGNDにつながってLowになる。 トリガーとしてはこのHighからLowに変わる「GPIO.FALLING」を設定し、push_eventを呼び出す。 ボタンを押したときに反応し、ボタンを離したときは反応しない。 このトリガー設定はほかにも「GPIO.RISING(立ち上がりエッジ検知)」「GPIO.BOTH(立ち上が/立下り両方)」などがある。
bouncetimeは、スイッチのチャタリングのための設定で、200ミリ秒間は続くイベントを無視してくれる。
これを使えば、今までの処理にadd_event_detectを追加しておいて、イベント処理を記述すれば、 簡単に改造が可能になりそう。これは便利。
ということで、初期処理にadd_event_detectを入れて、イベント登録しておき、 イベント処理として、ボタンが押された時の動作を作成する。 簡単にわかるように、7セグで使用していない「どっと」を表示してみよう。
~
#初期処理でイベント登録
GPIO.add_event_detect(17, GPIO.FALLING, callback=push_event, bouncetime=200)
~
def push_event(channel):
GPIO.output(gpio_dot, GPIO.HIGH)ボタンを押すと7セグのドットの箇所を表示させるだけ。一度押すと元には戻らないが、 これで、動作確認ができた。 呼び出されるコールバック関数の引数には、gpio番号は入るようで、いくつかのGPIOで関数を共用することもできそうだ。
シャットダウン機能
ボタンが4つあり、これを読み込むことができるようになったが、さてどのように利用しようか?
まずは簡単なところで、ラズベリーパイのシャットダウンスイッチとして機能させよう。

手前にあって押しやすい元ADJボタンをシャットダウンボタンとする。 押すといきなりシャットダウンでは、いろいろと間違いが起きそうなので、長押しするとシャットダウンする方式とする。 長押し検知は200ms毎ボタン状態をチェックして、15回(計3秒)とも押されている状態だったら、OKとし、 途中一度でも押されていない状態を検知したらキャンセルとする。
import os
~
# ADJボタンが押されたらイベント発生(ShutDown)
GPIO.add_event_detect(gpio_ADJ, GPIO.FALLING, callback=shut_down, bouncetime=200)
~
# シャットダウン処理
def shut_down(channel): # ボタン長押しでシャットダウン
if channel == gpio_ADJ:
sw = 0
for _ in range(15):
time.sleep(0.2) # 0.2秒 × 15回 = 3秒
sw = GPIO.input(channel)
if sw == 1: # 15回中1回でもLOWがあればシャットダウンしない
GPIO.output(gpio_dot, GPIO.LOW)
break
else:
GPIO.output(gpio_dot, GPIO.HIGH)
if sw == 0:
print("Shut Down Start")
GPIO.output(gpio_yellow, GPIO.HIGH)
os.system('sudo shutdown -h now') # シャットダウンボタンが押されているのを検知している間は、7セグのドットも表示して、 ボタン押下の検知がされていることをわかるようにしておく。 そして、シャットダウンモードになったら、黄色LEDを点灯させて、シャットダウンモード移行がわかるようにした。

ブザー
秋月時計には、ブザーがついているので、これを鳴らしてみる。 ジャンパーピンでXoutで鳴らすか、Youtで鳴らすか選択可能となっており、ひとまずXoutに接続。 XoutはGPIO23につながっているので、これを操作することで、ブザーを操作できる。
LED操作同様、Highにすればピーと音が鳴り、Lowにすると止まる。操作は簡単だ。
#!/usr/bin/env python3
import RPi.GPIO as GPIO # RPi.GPIOモジュールを使用
import time
# GPIO(Output)番号
gpio_X = 23
# GPIO(Input)番号
gpio_MODE = 9
# 初期処理
def setup():
# GPIO番号指定の準備
GPIO.setmode(GPIO.BCM)
# GPIO出力に設定
GPIO.setup(gpio_X , GPIO.OUT)
# GPIOの入力設定
GPIO.setup(gpio_MODE, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(gpio_MODE, GPIO.FALLING, callback=buzzer, bouncetime=100)
# ブザー
def buzzer(channel):
for _ in range(30):
time.sleep(0.1)
sw = GPIO.input(channel)
if sw == 1:
GPIO.output(gpio_X, GPIO.LOW)
break
else:
GPIO.output(gpio_X, GPIO.HIGH)
GPIO.output(gpio_X, GPIO.LOW)
# 終了処理
def destroy():
# GPIOを解放
GPIO.cleanup()
if __name__ == '__main__':
# start clock print
setup()
try:
while True:
time.sleep(0.001)
except KeyboardInterrupt:
# Ctrl + C
print("Keybord Interrupt occurred!")
finally:
print("Finished!")
destroy()PWM使用
PWMを使って、ブザーに周波数をつける。すると音階表現が可能らしい。
出力指定のGPIOに対してGPIO.PWM(ピン番号, 周波数)で、周波数を指定。
.start(Duty比率)で、PWM開始。
.stop()で、停止となる。
.ChangeFrequency(周波数)で、PWM周波数の変更が可能。
どのように変化するのか、各ボタンに周波数を割り当てて試してみる。
#!/usr/bin/env python3
import RPi.GPIO as GPIO # RPi.GPIOモジュールを使用
import time
# GPIO(Output)番号
gpio_X = 23
# GPIO(Input)番号
gpio_ADJ = 17
gpio_DOWN = 27
gpio_UP = 10
gpio_MODE = 9
# 初期処理
def setup():
# GPIO番号指定の準備
GPIO.setmode(GPIO.BCM)
# GPIO出力に設定
GPIO.setup(gpio_X , GPIO.OUT)
# GPIOの入力設定
GPIO.setup(gpio_MODE, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(gpio_ADJ, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(gpio_UP, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(gpio_DOWN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# ボタンのイベント登録
GPIO.add_event_detect(gpio_MODE, GPIO.FALLING, callback=buzzer, bouncetime=100)
GPIO.add_event_detect(gpio_ADJ, GPIO.FALLING, callback=buzzer, bouncetime=100)
GPIO.add_event_detect(gpio_UP, GPIO.FALLING, callback=buzzer, bouncetime=100)
GPIO.add_event_detect(gpio_DOWN, GPIO.FALLING, callback=buzzer, bouncetime=100)
# ブザー
def buzzer(channel):
# 440HzでPWMを設定
pwm = GPIO.PWM(gpio_X,440)
for _ in range(30):
time.sleep(0.1)
sw = GPIO.input(channel)
if sw == 1:
pwm.stop()
break
else:
if channel == gpio_MODE:
pwm.ChangeFrequency(440)
elif channel == gpio_ADJ:
pwm.ChangeFrequency(880)
elif channel == gpio_UP:
pwm.ChangeFrequency(1320)
elif channel == gpio_DOWN:
pwm.ChangeFrequency(1760)
# Duty比50で音を鳴らす
pwm.start(50)
GPIO.output(gpio_X, GPIO.LOW)
# 終了処理
def destroy():
# GPIOを解放
GPIO.cleanup()
if __name__ == '__main__':
# start clock print
setup()
try:
while True:
time.sleep(0.001)
except KeyboardInterrupt:
# Ctrl + C
print("Keybord Interrupt occurred!")
finally:
print("Finished!")
destroy()それぞれのボタンを押してみると、確かに音が違う。 それぞれを1オクターブ変化させたつもりだが、そこまでよい音階とはなっていないので、 正確な音階までは難しそうだ。
時報
ブザーによるメロディまでは難しいが、簡単な音は鳴らせるので、時報として使ってみる。 あまり長く音を鳴らせると鬱陶しいと思うので、ピッと簡単に鳴らしてみる。
正時にはピッピッと2回、半時にはピッと1回鳴らす。 時刻表示ループの中に入れて、正時や半時が来たら鳴らすようにする。 その時刻で一度鳴ったら、次までは鳴らないようにしておく。 あと、夜中は鳴らないようにしておく。
~
# buzzer
bz = 0
silent = ["00","01","02","03","04","05","06"]
~
# ブザー(時報)
if mm == "00": # 正時の場合
if bz == 0: # ブザー未実行の場合
GPIO.output(gpio_X, GPIO.HIGH)
time.sleep(0.03)
GPIO.output(gpio_X, GPIO.LOW)
time.sleep(0.1)
GPIO.output(gpio_X, GPIO.HIGH)
time.sleep(0.03)
GPIO.output(gpio_X, GPIO.LOW)
bz = 1 # ブザー鳴らし済
elif mm == "30": # 半時の場合
if bz == 0: # ブザー未実行の場合
GPIO.output(gpio_X, GPIO.HIGH)
time.sleep(0.03)
GPIO.output(gpio_X, GPIO.LOW)
bz = 1 # ブザー鳴らし済
else:
if hh in silent: # ブザーを鳴らさない時間帯
bz = 1
else:
bz = 0 # 鳴らし済フラグリセット
~ブザーの鳴らす長さや、2回鳴らす場合の間の時間などは、 色々聞き比べて、うるさすぎず、鬱陶しくない程度の値とした。 ちょっとだけ気づく程度の時報は、結構あると便利。なかなか良い機能だ。
Webサーバ(APIサーバ)
通常動作する時計は完成した。
本体にボタンもあるので、ある程度操作が可能となるが、遠隔操作もできると便利な気がするので、 Webサーバー化してAPIを実装してみたいと思う。
pythonでwebサーバが作れるか調べてみると、わりと簡単に作成できそう。 APIサーバを実装するためのパッケージ類がいくつか存在するようで、 「Django」「Flask」「FastAPI」などが有名どころらしい。
しかしあまりしっかりしたAPIサーバを作るつもりではないので、標準装備で作れないかな?
pythonの標準ライブラリとして「wsgiref」というのがあるらしく、WSGI仕様(Web Server Gateway Interface ウィスキー?)に 従ったWebサーバを実装できるらしい。これを使って構築してみる。
#!/usr/bin/env python3
from wsgiref import simple_server
# Web処理
def app(env, start_response):
start_response('200 OK', [
('Content-type', 'text/plain; charset=utf-8'),
('Access-Control-Allow-Origin', '*'), # 許可するアクセス
('Access-Control-Allow-Methods', '*'), # 許可するメソッド
],)
return [b'Hello, World.']
def web_setup():
httpd = simple_server.make_server('', 5000, app)
httpd.serve_forever()
if __name__ == '__main__':
web_setup()これだけで、webサーバができた。簡単にアクセスしてみる。
curl http://localhost:5000 Hello, World.
どうやらこれでよさそう。
スレッド化
単純なWebサーバを作るだけなら簡単なのだが、シングルスレッドで動作しているらしく、 時計処理に組み込むと、時計が動作しなくなった。Webサーバ処理はスレッド化してやらないと、 時計動作と同居は難しい。
なので、Webサーバのスレッド動作化を行う。 こちら を参考にWebサーバをスレッド起動にした。
#!/usr/bin/env python3
import time
import threading
from wsgiref.simple_server import make_server
from wsgiref.simple_server import WSGIServer
from socketserver import ThreadingMixIn
class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
pass
# Web処理
def web_setup():
# スレッドでWebサーバ起動
get_app_thread = threading.Thread(target=start_web_server)
get_app_thread.setDaemon(True)
get_app_thread.start()
# 初期処理(Webサーバ)
def start_web_server():
with make_server('', 5000, app) as httpd:
print("Web Server Started")
httpd.serve_forever()
# Web処理
def app(env, start_response):
start_response('200 OK', [
('Content-type', 'text/plain; charset=utf-8'),
('Access-Control-Allow-Origin', '*'), # 許可するアクセス
('Access-Control-Allow-Methods', '*'), # 許可するメソッド
],)
return [b'Hello, World.']
if __name__ == '__main__':
web_setup()
# 無限ループ
while True:
time.sleep(1)これを時計処理に組み込んでやると、時計処理とWeb処理が同居できた。 ひとまずAPIサーバ構築はできたようだ。 APIでの処理を実装しよう。
工事中
#!/usr/bin/env python3
import RPi.GPIO as GPIO # RPi.GPIOモジュールを使用
import sys
import os
import signal
import time
import datetime
from zoneinfo import ZoneInfo
import threading
from wsgiref.simple_server import make_server
from wsgiref.simple_server import WSGIServer
from socketserver import ThreadingMixIn
class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
pass
# 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
# GPIO(Input)番号
gpio_ADJ = 17
gpio_DOWN = 27
gpio_UP = 10
gpio_MODE = 9
# WaitTime
wait_time = 0.003
# buzzer
bz = 0
silent = ["00","01","02","03","04","05","06"]
# 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]
]
# 初期処理(Webサーバ)
# 参考 https://rikoubou.hatenablog.com/entry/2021/05/24/182745
def web_setup():
# スレッドでWebサーバ起動
get_app_thread = threading.Thread(target=start_web_server)
get_app_thread.setDaemon(True)
get_app_thread.start()
def start_web_server():
with make_server('', 3000, app) as httpd:
print("Web Server Started")
httpd.serve_forever()
# Web処理
def app(env, start_response):
start_response('200 OK', [
('Content-type', 'text/plain; charset=utf-8'),
('Access-Control-Allow-Origin', '*'), # 許可するアクセス
('Access-Control-Allow-Methods', '*'), # 許可するメソッド
],)
return [b'Hello, World.']
# 初期処理(GPIO関連)
def gpio_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)
GPIO.output(gpio_koron, GPIO.LOW)
GPIO.output(gpio_dot, GPIO.LOW)
GPIO.output(gpio_yellow, GPIO.LOW)
GPIO.output(gpio_X, GPIO.LOW)
GPIO.output(gpio_Y, GPIO.LOW)
# GPIOの入力設定
GPIO.setup(gpio_ADJ , GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(gpio_DOWN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(gpio_UP , GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(gpio_MODE, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# ADJボタンが押されたらイベント発生(ShutDown)
GPIO.add_event_detect(gpio_ADJ, GPIO.FALLING, callback=shut_down, bouncetime=200)
GPIO.add_event_detect(gpio_MODE, GPIO.FALLING, callback=buzzer, bouncetime=100)
GPIO.add_event_detect(gpio_UP, GPIO.FALLING, callback=push_event, bouncetime=200)
GPIO.add_event_detect(gpio_DOWN, GPIO.FALLING, callback=push_event, bouncetime=200)
print("Clock Started")
# ボタン押下処理
def push_event(channel):
if channel == gpio_UP:
GPIO.output(gpio_dot, GPIO.HIGH)
else:
GPIO.output(gpio_dot, GPIO.LOW)
# ブザー
def buzzer(channel):
pwm = GPIO.PWM(gpio_X,200)
for _ in range(30):
time.sleep(0.1)
sw = GPIO.input(channel)
if sw == 1:
# GPIO.output(gpio_X, GPIO.LOW)
pwm.stop()
break
else:
# GPIO.output(gpio_X, GPIO.HIGH)
pwm.start(50)
GPIO.output(gpio_X, 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 shut_down(channel): # ボタン長押しでシャットダウン
if channel == gpio_ADJ:
sw = 0
for _ in range(15):
time.sleep(0.2) # 0.2秒 × 15回 = 3秒
sw = GPIO.input(channel)
if sw == 1: # 15回中1回でもLOWがあればシャットダウンしない
GPIO.output(gpio_dot, GPIO.LOW)
break
else:
GPIO.output(gpio_dot, GPIO.HIGH)
if sw == 0:
print("Shut Down Start")
GPIO.output(gpio_yellow, GPIO.HIGH)
os.system('sudo shutdown -h now') # シャットダウン
# 終了処理
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 mm == "00":
if bz == 0:
GPIO.output(gpio_X, GPIO.HIGH)
time.sleep(0.03)
GPIO.output(gpio_X, GPIO.LOW)
time.sleep(0.1)
GPIO.output(gpio_X, GPIO.HIGH)
time.sleep(0.03)
GPIO.output(gpio_X, GPIO.LOW)
bz = 1
elif mm == "30":
if bz == 0:
GPIO.output(gpio_X, GPIO.HIGH)
time.sleep(0.03)
GPIO.output(gpio_X, GPIO.LOW)
bz = 1
else:
if hh in silent:
bz = 1
else:
bz = 0
if __name__ == '__main__':
# start clock print
gpio_setup()
web_setup()
try:
loop()
destroy()
except KeyboardInterrupt:
# Ctrl + C
print("Keybord Interrupt occurred!")
finally:
print("Finished!")
destroy()

