古いデジタルフォトフレームをhomekit対応な在室表示板にする

投稿者: | 12月 8, 2024

目次

はじめに

以前に
Sonyのデジタルフォトフレーム(DPF-HD800 DPF-D92)を分解してみた
という記事を書きましたが、このときに在室表示板にしてみたいという話をしましたが、それの続きです。このデジタルフォトフレームには全くWiFiがついていないので、それが課題でした。

仕組み

大まかな構成図は以下の感じです。

古いフォトフレームをhomekit対応な在室表示板にする
構成図 ブロック図
ブロック図

WiFi_shareable_SDcardReaderとそのAPI、Homebridge-cmd4あたりがこのプロジェクトの中核部分です。

デジタルフォトフレーム(Sony DPF-HD800)

詳しくは、
Sonyのデジタルフォトフレーム(DPF-HD800 DPF-D92)を分解してみた
で書いてあります。公式の主な仕様
2013年発売のWiFi機能がない、シンプルなデジタルフォトフレームです。
入力端子は、SDカードとメモリースティックとUSB(ホスト、デバイス両方)です。今回はUSB(ホスト)を使用します。

WiFi_shareable_SDcardReader

詳しくは、
WiFi経由のブラウザと同時アクセスもできるUSB SDカードリーダ(WiFi_shareable_SDcardReader)をESP32 S3・S2・Raspberry Pi Pico Wで作ってみた
で書いてあります。
USBメモリと振る舞いつつ、WiFiからも内容を変更できるものです。ESP32 S3で作成しました。これをデジタルフォトフレームに刺して、サーバからUSBの内容を書き換える感じです。

設定としては、USB_REFRASH、REFRESH_TIME_LENGTH 10000としました。

Homebridge

言わずもがな知れた、あらとあらゆるIoT機器をHomekitに対応化させることができるソフトウェアです。プラグインをいれることで様々な方法でデバイスを追加できます。
今回はDebian上で動かしています。

Homebridge-cmd4

Homebridgeのプラグインで、任意のコマンドに引数をつけて実行して、標準出力の結果と戻り値を利用してデバイスを操作するものです。詳しくは、
Qiita @n1330「homebridge-cmd4 設定例」https://qiita.com/n1330/items/f5ede320e9b5963bccd1
が参考になります。任意のデバイスの種類に対応していて、非常に使いやすいです。CMD4 Portalというデバイスやパラメータの一覧がまとめられているサイトがあるので楽です。昔に一度使った、homebridge http advanced accessoryを使おうと思ったのですが、難しかったのでやめました。

今回は、Televisonを使用して、入力切替のところで、表示の切り替えができるようにしました。

Homebridgeのconfig.jsonはこんな感じです。入力切替先の数だけInputSourceを増やさないといけないため、かなり長くなってしまいました。

,
        {
            "platform": "Cmd4",
            "name": "Cmd4",
            "accessories": [
                {
                    "type": "Television",
                    "configuredName": "在室表示板",
                    "displayName": "Occupancy_Sign",
                    "active": "ACTIVE",
                    "activeIdentifier": 1,
                    "sleepDiscoveryMode": "ALWAYS_DISCOVERABLE",
                    "remoteKey": "SELECT",
                    "publishExternally": true,
                    "category": "TELEVISION",
                    "linkedTypes": [
                        {
                            "type": "InputSource",
                            "configuredName": "在室",
                            "displayName": "Input1",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 1,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "不在",
                            "displayName": "Input2",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 2,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "調整中",
                            "displayName": "Input3",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 3,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "部屋外",
                            "displayName": "Input4",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 4,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "外出中",
                            "displayName": "Input5",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 5,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "就寝中",
                            "displayName": "Input6",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 6,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "会議中",
                            "displayName": "Input7",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 7,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "バイト中",
                            "displayName": "Input8",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 8,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "取込中",
                            "displayName": "Input9",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 9,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        },
                        {
                            "type": "InputSource",
                            "configuredName": "実験中",
                            "displayName": "Input10",
                            "currentVisibilityState": "SHOWN",
                            "inputSourceType": "USB",
                            "isConfigured": "CONFIGURED",
                            "identifier": 10,
                            "targetVisibilityState": "SHOWN",
                            "polling": true,
                            "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                        }
                    ],
                    "polling": [
                        {
                            "characteristic": "active",
                            "interval": 60,
                            "timeout": 60000
                        },
                        {
                            "characteristic": "remoteKey"
                        },
                        {
                            "characteristic": "activeIdentifier"
                        }
                    ],
                    "state_cmd": "python3 -u /home/unagidojyou/occupancy_sign/occupancy_sign.py"
                }
            ]
        }

state_cmdが引数を付けて実行されるコマンドです。今回は、Pythonスクリプトを実行するので、長めになっています。Pythonに渡される引数はGetコマンドのときは、

Get 'Input1' 'CurrentVisibilityState'
Get 'Occupancy_Sign' 'Active'
Get 'Occupancy_Sign' 'RemoteKey'

みたいな感じです。Get "DisplayName" "内容"っぽいです。
Setコマンドのときは、

Set Occupancy_Sign Active 1
Set Occupancy_Sign RmoteKey 8
Set Occupancy_Sign ActiveIdentifier 1

みたいな感じです。Set "DisplayName" "項目" "数字"っぽいです。

Pythonスクリプト

Homebridge-cmd4から呼び出されるスクリプト自体はこんな感じです。WiFi_shareable_SDcardReaderのフロントエンドはArduinoIDE_SD_FAT32_Fileserverと全く同様なので、そのAPI(ArduinoIDE_SD_FAT32_Fileserver_API.py)を使用しています。

Getコマンドに関して
・InputのCurrentVisibilityStateは、常に標準出力に0を返します。
・Occupancy_SignのRemoteKeyは、リモコンのボタンに関するものですが、リモコンは存在しないので常にディフォルト値の8を返します。
・Occupancy_SignのActiveは、ルートディレクトリが存在すれば、1(ON)を返し、何かしら失敗した場合は0(OFF)を返します。

Setコマンドに関して、常にInputに対してSetはないので常にOccupancy_Signに関するものです。
・Activeは、常に電源がONでON/OFFは制御できないのでGetのActiveと同様な動作をします。
・RemoteKeyも、Getと同様で常に8で、何も行いません。
・ActiveIdentifierは、入力切替に関することなので、対応した番号の写真に切り替わるようにします。

各種操作に失敗した場合は、sys.exit(-1)でHomebridge-cmd4にエラーを知らせます。

画像を切り替える仕組みですが、SDカードにすべての画像ファイルを入れて、表示させたい画像のみ、拡張子をjpgにし、それ以外の画像の拡張子をdjpgにしています。名前の変更だけなので、いちいち、画像をアップロードしたり削除する手間がないため通信量を抑えることができます。

import ArduinoIDE_SD_FAT32_Fileserver_API as SD_API
import sys
import os

def get_active():
    if SD_API.check_path_exist('/'):
        return 1
    else:
        return 0

def get_remoteKey():
    return 8

def get_activeIdentifier():
    root_dir = SD_API.get_file_dir('/')
    if root_dir == False:
        print('could not get dir list', file=sys.stderr)
        return 0 #エラー時
    for filename in root_dir:
        if not filename[-1] == '/':
            if os.path.splitext(os.path.basename(filename))[1] == '.jpg':
                return jpg_older.index(os.path.splitext(os.path.basename(filename))[0])
    print('could not find .jpg file', file=sys.stderr)
    return 0 #jpgファイルが見つからないとき

def set_active(state):
    return get_active()

def set_remoteKey(state):
    return get_remoteKey()

def set_activeIdentifier(state):
    root_dir = SD_API.get_file_dir('/')
    if root_dir == False:
        print('could not get dir list', file=sys.stderr)
        return 0 #エラー時
    for filename in root_dir:
        if not filename[-1] == '/':
            file_ext = os.path.splitext(os.path.basename(filename))[1]
            file_name = os.path.splitext(os.path.basename(filename))[0]
            if file_ext == '.jpg' or file_ext == '.djpg':
                if (file_name == jpg_older[state]) and (file_ext == '.djpg'): # djpg->jpg
                    if not SD_API.change_name('/' + filename, '/' + file_name + '.jpg'):
                        print('could not change ' + filename, file=sys.stderr)
                        return 0
                elif (file_ext == '.jpg') and (not file_name == jpg_older[state]):
                    if not SD_API.change_name('/' + filename, '/' + file_name + '.djpg'):
                        print('could not change ' + filename, file=sys.stderr)
                        return 0
    return get_activeIdentifier()

args = sys.argv
if len(args) < 1:
    print('arg is required')
request = args[1]
if request == 'Get' and (not len(args) == 4):
    print('worong args', file=sys.stderr)
    sys.exit(-1)
elif request == 'Set' and (not len(args) == 5):
    print('worng args', file=sys.stderr)
    sys.exit(-1)
name = args[2]
characteristic = args[3]
if request == 'Set':
    state = int(args[4])

SD_API.set_ipaddr('192.168.0.0')

# homebridgeのidentifierと番号を揃える。ファイル名と同じものを入力
jpg_older = ['', '在室', '不在', '調整中', '部屋外', '外出中', '就寝中', '会議中', 'バイト中', '取込中', '実験中']

activeIdentifier = 1

if request == 'Get':
    if name == 'Occupancy_Sign':
        if characteristic == 'Active':
            print(get_active())
        elif characteristic == 'RemoteKey':
            print(get_remoteKey())
        elif characteristic == 'ActiveIdentifier':
            print(get_activeIdentifier())
    elif name[:5] == 'Input':
        print(0)
    sys.exit(0)
elif request == 'Set':
    if characteristic == 'Active':
        print(set_active(state))
    elif characteristic == 'RemoteKey':
        print(set_remoteKey(state))
    elif characteristic == 'ActiveIdentifier':
        print(set_activeIdentifier(state))
    sys.exit(0)

jpg_olderがファイル名と、Homebridge-cmd4のconfig.jsonに記した、”identifier”を結びつけます。jpg_older[“identifier”]がjpgファイルの名前と一致する必要があります。今回は日本語のファイル名なので、上のコードの様になっています。

画像の用意

画像はパワーポイントで作成しました。DPF-HD800の解像度が800×480で、ピクセル数に0.0264を掛けたものがパワーポイントのスライドのサイズ(cm)なようなので、幅を21.118cm、高さを12.674cmに設定しました。CubePDFとかを使ってJPEGに変換しました。Pythonスクリプトのjpg_olderに合わせて日本語なファイル名で保存しました。

完成したもの

おわりに

作ってから2週間以上経ちますが問題なく動いていていい感じです。なかなか便利です。

今回のプロジェクトで一番大変だったのは、Python APIの作成です。直接は関係ないので、この記事には書いていないのですが、かなり時間がかかりました。結果的にかなり信頼性の高いArduinoIDE_SD_FAT32_Fileserver_API.pyが書けたので良かったです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)