前書き
Raspberry pi4bもjetson nanoも定価で全然売っておらず、買えてません。欲しい・・・(前回に続いて2回目。)
前回の記事LINE BOT経由でパパの居場所(androidスマホ)を地図で教える(成功編)に続いて、TVとHDMIでつながっているRaspberry piを活用してTVを制御してみます。
子供が学校から帰ってきて、TVばっかり見て困っているので、考えてみました。
TVの電源ON/OFFのログが見れたり、TVを起動禁止(10秒周期でTV電源のON/OFFを検知し、ONだったら消す)にしたりできます。
ご指摘・ご質問あれば、コメントいただければと思います。
構成&動作
TVとRaspberry piをHDMIで接続します。
Raspberry piは、CEC対応なので、そのまま使用します。
確認者がLINEでTVの状態の確認、TVの過去の電源ON/OFFの履歴および禁止のON/OFFが設定できます。
docker内でpythonを使用して、libCECライブラリ経由でTV情報を取得しています。
TVの過去の電源ON/OFFの履歴および禁止のON/OFFの設定については、firebaseのFirestore Databaseに保存しています。
複数ホストでdockerを動作させているため、オーケストレーションツールの導入を一度は検討しましたが、断念しました。
Docker SWARM: docker-compose.ymlのdevices:の記載が機能しなくなるため、見送りました。
k8s: メモリを使いすぎるので、導入できず。(Raspberry Pi4欲しい・・・。)
構築
環境の構築については、
記事LINE BOT経由でRaspberry Piに接続したUSBカメラの画像を取得してみるを参照ください。
また、前回の記事LINE BOT経由でパパの居場所(androidスマホ)を地図で教える(成功編)でFirebaseのCloud Firestore(Cloud Messagingに使用するトークンの保存に使用)を使用しましたので、今回もTVの禁止設定とログに使用したいと思います。
CEC制御部分のpythonコード
今回の肝であるCECの制御部分の説明です。
python3-cecパッケージに含まれる/usr/share/doc/python3-cec/examples/pyCecClient.pyを参考にしました。
Raspberry pi2で何故か、TVのステータスが何故かunknownになってしまう、/dev/cec0を見に行くなど発生しました。
色々調べてみると、/boot/configのdtoverlay=vc4-kms-v3dで指定しているドライバが原因と分かりました。
/boot/config.txtで、"dtoverlay=vc4-kms-v3d"となっていないかチェックしてください。
もし、なっていれば、"dtoverlay=vc4-fkms-v3d"に変更して試してみてください。
# for cec
import cec
# for flask
from flask import Flask, jsonify
# for environment
import os
app = Flask(__name__)
CECの制御部分はWebAPI形式で提供するので、Flaskをインポートしています。
あと、もちろんCECもインポートします。
# create new libcec_configuration
cecconfig = cec.libcec_configuration()
# set configulation
set_config(cecconfig, cec_control_name)
# set keypress callback
set_keypresscallback(cecconfig, key_press_callback)
# Initialise
adapter = cec.ICECAdapter.Create(cecconfig)
adapter.InitVideoStandalone()
# Open adapter
adapters = adapter.DetectAdapters()
if (len(adapters) < 1):
print("can not find adapter")
exit(0)
# get my adapter info
my_adapter = adapters[0]
print("my_adapter is :")
print(" port: " + my_adapter.strComName)
print(" vendor: " + hex(my_adapter.iVendorId))
print(" product: " + hex(my_adapter.iProductId))
adapter_name = my_adapter.strComName
if adapter.Open(adapter_name):
print("connection opened.")
else:
print("unable to open the device on port " + adapter_name)
exit(0)
CECの初期化部分です。
ほぼ定型です。
# Get TV's power status
@app.route('/GetTVPowerStatus', methods=['GET'])
def getTVPowerStatus():
power = adapter.GetDevicePowerStatus(0)
pow_str = adapter.PowerStatusToString(power)
json = {
'status': pow_str,
'name': cec_control_name
}
return jsonify(json)
初期化で取得したadapterでGetDevicePowerStatus(0)を読んでます。
0は論理アドレスでTVが割り当てられていますので、テレビの電源状態が取得できます。
取得できるのは数字のため、人間が読めるようにadapter.PowerStatusToString(power)で変換後、
名前とセットでjsonにして返しています。
電源が入っている場合は"on"、電源が切れている場合はstandbyが取れます。
# Set TV's power
@app.route('/SetTVPower/<string:controlType>', methods=['PUT'])
def setTVPower(controlType):
result = False
if controlType == 'on':
result = adapter.PowerOnDevices(0)
elif controlType == 'off':
result = adapter.StandbyDevices(0)
json = {
'result': result,
'name': cec_control_name
}
return jsonify(json)
URLでon,offを指摘すると、電源のON,OFFが出来ます。
adapter.PowerOnDevices(0)、adapter.StandbyDevices(0)ですので、ターゲットはTVとなります。
@app.route('/GetActiveSource', methods=['GET'])
def getActiveSource():
activeSource = adapter.GetActiveSource()
print('activeSource is ', activeSource)
as_str = adapter.LogicalAddressToString(activeSource)
json = {
'activesource': as_str,
'name': cec_control_name
}
return jsonify(json)
adapter.GetActiveSource()で現在、テレビに表示しているソースを取得できます。
ちなみに本APIはどこからも呼ばれておりません。
# Be Active Source
@app.route('/BecomeActiveSource/<string:target>', methods=['PUT'])
def becomeActiveSource(target):
result = False
if target == 'TV':
result = adapter.SetActiveSource(0)
else:
# If target is other than TV, my device will be active source.
result = adapter.SetActiveSource()
json = {
'result': result,
'name': cec_control_name
}
return jsonify(json)
adapter.SetActiveSource()で引数に指定した論理アドレスの機器入力を有効にします。
ですので、TVであれば0を指定します。
ちなみに指定しなければ、自身(Raspberry Piの入力)が有効になります。
ちなみに本APIはどこからも呼ばれておりません。
これらのWebAPIを外部から呼び出すことにより、TV操作を制御します。
CEC制御部分のdockerイメージ作り
dockerイメージのbaseは、Raspbianでないといけないです。raspberry用のlibcec用ドライバとかを入れるので。
ですが、探したのですが、公式のイメージは少し古いですので、現時点で最新のBullseyeを使用したイメージを作成します。
$ docker image import https://downloads.raspberrypi.org/raspios_lite_armhf/root.tar.xz raspiarm32-lite
Downloading from https://downloads.raspberrypi.org/raspios_lite_armhf/root.tar.xz
Importing [==================================================>] 311.3MB/311.3MB
sha256:ee3e4b69b70506aab7751d8922bc8cd6e7741dfd3132c76a1f41d8f548194f2d
$
raspberry piの公式から、raspberry pi os のlite(GUIが無いやつ)のroot.tar.gzをインポートして、イメージを作成します。
FROM raspiarm32-lite:latest
ENV TOP_DIR /cec_control
RUN mkdir ${TOP_DIR}
COPY cec_control.py ${TOP_DIR}
WORKDIR ${TOP_DIR}
RUN apt-get update && apt-get install -y \
libcec6 \
python3-cec \
python3-flask
CMD ["/usr/bin/python3","cec_control.py"]
libcec6はlibcec関連のパッケージ、python3-cecはpythonのlibcecバインディングとかが入っているpythonからcecを使うのに必要なパッケージです。
cec_control_LIVING:
image: docker_cec_control:latest
restart: always
networks:
- docker_net
environment:
CEC_CONTROL_HOST: "cec_control_LIVING"
CEC_CONTROL_PORT: "3001"
CEC_CONTROL_NAME: "LIVING"
devices:
- "/dev/vchiq:/dev/vchiq"
docker_cec_controlは、上のDockerfileから作成されたイメージです。
"/dev/vchiq:/dev/vchiq"
Raspberry PiのlibCECは、/dev/vchiqデバイスが必要のようなので、docker上からも触れるように設定します。
あとは、外部から制御する部分だけですので、詳細は割愛します。
ソースコードをご確認ください。
CEC制御のソースコード:cec_control
heroku上のソースコード:tvca-line-bot
docker上のjavascriptのソースコードTVCArouter
総括
cec-clientを使用してTVを制御する記事はよく見かけますが、pythonからAPIを呼び出す記事が少なかったので、書いてみました。
この記事が少しでも、他の方の役に立てば幸いです。